本文基于 HarmonyOS 6.1.1 (API 24) 最新特性,深入讲解如何使用 Tabs + List + Grid 组合构建高性能、高交互性的复杂首页。

项目演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

目录


一、引言:为什么需要复杂布局

在移动应用开发中,首页是用户进入应用后看到的第一个界面,其重要性不言而喻。一个优秀的首页应该具备以下特点:

  • 信息密度高:能够在有限屏幕空间展示尽可能多的内容
  • 交互流畅:支持多种手势操作,如滑动、点击、长按等
  • 视觉层次清晰:通过不同的布局方式区分内容优先级
  • 性能优异:即使内容丰富,也能保持流畅的滚动和动画效果

在 HarmonyOS 生态中,ArkUI 框架提供了丰富的布局组件来满足这些需求。其中,TabsListGrid 是构建复杂首页的三大核心组件:

  • Tabs:用于底部导航或顶部选项卡,实现多页面切换
  • List:用于线性排列的列表内容,支持横向和纵向滚动
  • Grid:用于网格排列的内容,适合展示商品、分类等

这三个组件的组合可以说是"黄金搭档"——Tabs 负责页面级导航,List 和 Grid 负责页面内的内容展示。

1.1 布局架构概览

我们要实现的首页采用经典的电商 App 布局:

┌─────────────────────────────────────────────┐
│  顶部状态栏                                    │
├─────────────────────────────────────────────┤
│  🔍 搜索栏                                    │
├─────────────────────────────────────────────┤
│  Banner 轮播(横向 List)                     │
├─────────────────────────────────────────────┤
│  ┌────┬────┬────┬────┐                       │
│  │分类│分类│分类│分类│  ← Grid 4列            │
│  └────┴────┴────┴────┘                       │
├─────────────────────────────────────────────┤
│  "为你推荐" 标题                               │
├─────────────────────────────────────────────┤
│  ┌──────────┬──────────┐                     │
│  │  商品 1  │  商品 2  │ ← Grid 2列           │
│  ├──────────┼──────────┤                     │
│  │  商品 3  │  商品 4  │                     │
│  └──────────┴──────────┘                     │
├─────────────────────────────────────────────┤
│ 🏠首页 │ 📋分类 │ 🛒购物车 │ 👤我的│          │
└─────────────────────────────────────────────┘
             Tabs 底部导航

1.2 技术栈说明

技术 说明
ArkTS HarmonyOS 官方推荐的应用开发语言,基于 TypeScript 扩展
ArkUI HarmonyOS 声明式 UI 框架,采用组件化设计
@State 状态管理装饰器,实现响应式数据驱动
LazyForEach 懒加载迭代器,用于大数据集的高效渲染
IDataSource 数据源接口,LazyForEach 的必需依赖

1.3 API 版本说明

本文示例代码基于 HarmonyOS 6.1.1 (API 24) 开发,相比之前的版本,API 24 在以下方面进行了重要增强:

  • Tabs 组件支持嵌套滚动
  • 动态布局容器(Dynamic Layout Container)
  • 更完善的 IDataSource 接口
  • 性能监控与优化工具

二、核心组件基础

2.1 Tabs 选项卡组件

Tabs 是实现多页面切换的容器组件,每个 Tab 对应一个 TabContent。

2.1.1 基础用法
Tabs({ index: 0 }) {
    TabContent() {
        Text('首页')
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
    }
    .tabBar('首页')

    TabContent() {
        Text('分类')
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
    }
    .tabBar('分类')
}
2.1.2 关键属性
属性 类型 默认值 说明
index number 0 当前选中的 Tab 索引
barPosition BarPosition End TabBar 位置
barHeight Length 56 TabBar 高度
barBackgroundColor ResourceColor - TabBar 背景色
animationDuration number 300 切换动画时长(ms)
onChange Callback<number> - Tab 切换回调
2.1.3 状态绑定
@State currentTabIndex: number = 0;

build() {
    Tabs({ index: this.currentTabIndex }) {
        TabContent() { /* ... */ }.tabBar('首页')
        TabContent() { /* ... */ }.tabBar('分类')
        TabContent() { /* ... */ }.tabBar('购物车')
        TabContent() { /* ... */ }.tabBar('我的')
    }
    .onChange((index: number) => {
        this.currentTabIndex = index;
    })
}

2.2 List 列表组件

List 是用于展示线性排列内容的容器组件,支持横向和纵向滚动。

2.2.1 横向 List 实现 Banner
List() {
    LazyForEach(this.bannerDataSource, (item: BannerItem) => {
        ListItem() {
            Row() {
                Column() {
                    Text(item.title)
                        .fontSize(24)
                        .fontWeight(FontWeight.Bold)
                        .fontColor(Color.White)
                }
                .alignItems(HorizontalAlign.Start)
                .padding({ left: 24, right: 24 })
            }
            .width('100%')
            .height('100%')
            .backgroundColor(item.color)
            .borderRadius(12)
        }
    }, (item: BannerItem) => item.id)
}
.listDirection(Axis.Horizontal)
.width('100%')
.height(160)
.padding({ left: 16, right: 16 })
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
2.2.2 关键属性
属性 类型 默认值 说明
listDirection Axis Vertical 列表方向
scrollBar BarState Auto 滚动条状态
edgeEffect EdgeEffect Spring 边缘效果
divider DividerStyle - 分割线样式
cachedCount number 1 缓存数量

2.3 Grid 网格组件

Grid 用于展示网格排列的内容,非常适合分类入口、商品展示等场景。

2.3.1 基础用法
Grid() {
    GridItem() {
        Text('商品 1').fontSize(16)
    }
    GridItem() {
        Text('商品 2').fontSize(16)
    }
    GridItem() {
        Text('商品 3').fontSize(16)
    }
    GridItem() {
        Text('商品 4').fontSize(16)
    }
}
.columnsTemplate('1fr 1fr')
.rowsGap(10)
.columnsGap(10)
.width('100%')
.padding(16)
2.3.2 columnsTemplate 高级用法
// 四列等宽
.columnsTemplate('1fr 1fr 1fr 1fr')

// 两列不等宽(3:7)
.columnsTemplate('0.3fr 0.7fr')

// 固定宽度 + 自适应
.columnsTemplate('100px 1fr 100px')

三、LazyForEach 与 DataSource 深度解析

LazyForEach 是 ArkUI 中用于大数据集高效渲染的核心机制,能够按需创建组件,避免一次性渲染所有数据。

3.1 IDataSource 接口规范

interface IDataSource {
    /** 获取数据总数 */
    totalCount(): number;
    
    /** 获取指定位置的数据 */
    getData(index: number): Object;
    
    /** 注册数据变化监听器 */
    registerDataChangeListener(listener: DataChangeListener): void;
    
    /** 注销数据变化监听器 */
    unregisterDataChangeListener(listener: DataChangeListener): void;
}

3.2 DataChangeListener 接口

interface DataChangeListener {
    /** 数据重新加载 */
    onDataReloaded(): void;
    
    /** 指定位置数据新增 */
    onDataAdd(index: number): void;
    
    /** 指定位置数据变化 */
    onDataChange(index: number): void;
    
    /** 指定位置数据删除 */
    onDataDelete(index: number): void;
    
    /** 数据位置移动 */
    onDataMove(from: number, to: number): void;
}

3.3 完整 DataSource 实现

export class BannerDataSource implements IDataSource {
    private dataList: BannerItem[] = [];
    private listeners: DataChangeListener[] = [];

    constructor(dataList: BannerItem[]) {
        this.dataList = dataList;
    }

    totalCount(): number {
        return this.dataList.length;
    }

    getData(index: number): BannerItem {
        return this.dataList[index];
    }

    registerDataChangeListener(listener: DataChangeListener): void {
        if (this.listeners.indexOf(listener) < 0) {
            this.listeners.push(listener);
        }
    }

    unregisterDataChangeListener(listener: DataChangeListener): void {
        const pos = this.listeners.indexOf(listener);
        if (pos >= 0) {
            this.listeners.splice(pos, 1);
        }
    }

    // 通知方法
    notifyDataReload(): void {
        for (let i = 0; i < this.listeners.length; i++) {
            this.listeners[i].onDataReloaded();
        }
    }

    notifyDataAdd(index: number): void {
        for (let i = 0; i < this.listeners.length; i++) {
            this.listeners[i].onDataAdd(index);
        }
    }

    notifyDataChange(index: number): void {
        for (let i = 0; i < this.listeners.length; i++) {
            this.listeners[i].onDataChange(index);
        }
    }

    notifyDataDelete(index: number): void {
        for (let i = 0; i < this.listeners.length; i++) {
            this.listeners[i].onDataDelete(index);
        }
    }

    notifyDataMove(from: number, to: number): void {
        for (let i = 0; i < this.listeners.length; i++) {
            this.listeners[i].onDataMove(from, to);
        }
    }
}

3.4 DataSource 动态更新

// 添加数据
addItem(item: BannerItem): void {
    this.dataList.push(item);
    this.notifyDataAdd(this.dataList.length - 1);
}

// 删除数据
removeItem(index: number): void {
    this.dataList.splice(index, 1);
    this.notifyDataDelete(index);
}

// 更新数据
updateItem(index: number, item: BannerItem): void {
    this.dataList[index] = item;
    this.notifyDataChange(index);
}

四、实战:电商首页完整实现

4.1 数据模型

// DataModels.ets

export class BannerItem {
    id: string;
    title: string;
    subtitle: string;
    imageUrl: string;
    color: string;
    jumpUrl: string;

    constructor(id: string, title: string, subtitle: string, 
                imageUrl: string, color: string, jumpUrl: string) {
        this.id = id;
        this.title = title;
        this.subtitle = subtitle;
        this.imageUrl = imageUrl;
        this.color = color;
        this.jumpUrl = jumpUrl;
    }
}

export class CategoryItem {
    id: string;
    name: string;
    icon: string;
    color: string;
    count: number;

    constructor(id: string, name: string, icon: string, 
                color: string, count: number) {
        this.id = id;
        this.name = name;
        this.icon = icon;
        this.color = color;
        this.count = count;
    }
}

export class ProductItem {
    id: string;
    name: string;
    description: string;
    price: number;
    originalPrice: number;
    imageUrl: string;
    salesCount: number;
    rating: number;
    tags: string[];

    constructor(id: string, name: string, description: string,
                price: number, originalPrice: number, imageUrl: string,
                salesCount: number, rating: number, tags: string[]) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.price = price;
        this.originalPrice = originalPrice;
        this.imageUrl = imageUrl;
        this.salesCount = salesCount;
        this.rating = rating;
        this.tags = tags;
    }
}

export class CartItem {
    id: string;
    productId: string;
    productName: string;
    price: number;
    quantity: number;
    imageUrl: string;
    selected: boolean;
    maxQuantity: number;

    constructor(id: string, productId: string, productName: string,
                price: number, quantity: number, imageUrl: string,
                selected: boolean, maxQuantity: number = 99) {
        this.id = id;
        this.productId = productId;
        this.productName = productName;
        this.price = price;
        this.quantity = quantity;
        this.imageUrl = imageUrl;
        this.selected = selected;
        this.maxQuantity = maxQuantity;
    }
}

4.2 数据生成器

// 生成 Banner 数据
export function generateBannerData(): BannerItem[] {
    const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
    const titles = ['限时特惠', '新品上市', '品牌专区', '满减活动', '会员专享'];
    const subtitles = ['爆款商品低至 5 折', '抢先体验', '大牌正品', '满199减50', '积分翻倍'];
    const banners: BannerItem[] = [];

    for (let i = 0; i < 5; i++) {
        banners.push(new BannerItem(
            `banner_${i}`, titles[i], subtitles[i], '', colors[i], `jump_${i}`
        ));
    }
    return banners;
}

// 生成分类数据
export function generateCategoryData(): CategoryItem[] {
    const icons = ['📱', '💻', '🎧', '⌚', '📷', '🎮', '👟', '🎒'];
    const names = ['手机数码', '电脑办公', '影音娱乐', '智能穿戴', 
                   '摄影摄像', '游戏设备', '运动户外', '箱包配饰'];
    const colors = ['#FFE5E5', '#E5F0FF', '#E5FFE5', '#FFFFE5', 
                    '#FFE5F0', '#E5FFFF', '#F0E5FF', '#FFF5E5'];
    const categories: CategoryItem[] = [];

    for (let i = 0; i < 8; i++) {
        categories.push(new CategoryItem(
            `cat_${i}`, names[i], icons[i], colors[i], Math.floor(Math.random() * 1000)
        ));
    }
    return categories;
}

// 生成商品数据
export function generateProductData(count: number, category: string): ProductItem[] {
    const products: ProductItem[] = [];
    const names = ['蓝牙耳机 Pro', '智能手表 S3', '便携充电器', '蓝牙音箱'];
    const descs = ['高品质音效', '健康监测', '快充技术', '360°环绕声'];
    const tags = [['新品'], ['限时优惠'], ['爆款'], ['会员价']];

    for (let i = 0; i < count; i++) {
        const price = Math.floor(Math.random() * 800 + 100);
        products.push(new ProductItem(
            `${category}_${i}`, names[i % names.length] + ` ${i+1}`, 
            descs[i % descs.length], price, price + 100, '',
            Math.floor(Math.random() * 10000), 4.5, tags[i % tags.length]
        ));
    }
    return products;
}

4.3 完整首页实现

// Index.ets
import { BannerItem, ProductItem, CategoryItem, CartItem } from './DataModels';
import { BannerDataSource, ProductDataSource, CategoryDataSource, CartDataSource } from './DataModels';
import { generateBannerData, generateProductData, generateCategoryData, generateCartData } from './DataModels';

@Entry
@Component
struct Index {
    // 状态
    @State currentTabIndex: number = 0;
    @State bannerList: BannerItem[] = [];
    @State productList: ProductItem[] = [];
    @State categoryList: CategoryItem[] = [];
    @State cartList: CartItem[] = [];
    @State cartTotalPrice: number = 0;
    @State cartSelectedCount: number = 0;

    // 数据源
    private bannerDataSource: BannerDataSource = new BannerDataSource([]);
    private productDataSource: ProductDataSource = new ProductDataSource([]);
    private categoryDataSource: CategoryDataSource = new CategoryDataSource([]);
    private cartDataSource: CartDataSource = new CartDataSource([]);

    aboutToAppear(): void {
        this.initData();
    }

    private initData(): void {
        this.bannerList = generateBannerData();
        this.productList = generateProductData(20, 'recommend');
        this.categoryList = generateCategoryData();
        this.cartList = generateCartData();

        this.bannerDataSource = new BannerDataSource(this.bannerList);
        this.productDataSource = new ProductDataSource(this.productList);
        this.categoryDataSource = new CategoryDataSource(this.categoryList);
        this.cartDataSource = new CartDataSource(this.cartList);

        this.calculateCartTotal();
    }

    private calculateCartTotal(): void {
        let total = 0;
        let count = 0;
        for (let i = 0; i < this.cartList.length; i++) {
            const item = this.cartList[i];
            if (item.selected) {
                total += item.price * item.quantity;
                count += item.quantity;
            }
        }
        this.cartTotalPrice = total;
        this.cartSelectedCount = count;
    }

    // ========== Builder ==========

    @Builder
    BannerItemBuilder(banner: BannerItem) {
        Row() {
            Column() {
                Text(banner.title)
                    .fontSize(22)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(Color.White)
                    .margin({ bottom: 6 })
                Text(banner.subtitle)
                    .fontSize(14)
                    .fontColor(Color.White)
                    .opacity(0.9)
            }
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 20, right: 20 })
        }
        .width('100%')
        .height('100%')
        .backgroundColor(banner.color)
        .borderRadius(12)
        .shadow({ radius: 8, color: '#20000000', offsetX: 0, offsetY: 4 })
    }

    @Builder
    CategoryItemBuilder(category: CategoryItem) {
        Column() {
            Column() {
                Text(category.icon).fontSize(28)
            }
            .width(52)
            .height(52)
            .backgroundColor(category.color)
            .borderRadius(26)
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
            .margin({ bottom: 8 })

            Text(category.name)
                .fontSize(13)
                .fontColor('#333333')
                .maxLines(1)
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 12, bottom: 12 })
    }

    @Builder
    ProductCardBuilder(product: ProductItem) {
        Column() {
            Column() {
                Text('📦').fontSize(48)
            }
            .width('100%')
            .height(140)
            .backgroundColor('#F5F5F5')
            .borderRadius({ topLeft: 8, topRight: 8 })
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)

            Column() {
                Text(product.name)
                    .fontSize(14)
                    .fontColor('#333333')
                    .maxLines(2)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                    .width('100%')
                    .margin({ bottom: 4 })

                Row() {
                    Text('¥').fontSize(12).fontColor('#FF4444')
                    Text(product.price.toString())
                        .fontSize(18).fontColor('#FF4444')
                        .fontWeight(FontWeight.Bold)
                    Text(`¥${product.originalPrice}`)
                        .fontSize(12).fontColor('#CCCCCC')
                        .decoration({ type: TextDecorationType.LineThrough })
                        .margin({ left: 6 })
                }
                .width('100%')
            }
            .width('100%')
            .padding(8)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(8)
        .shadow({ radius: 4, color: '#10000000', offsetX: 0, offsetY: 2 })
    }

    @Builder
    HomePage() {
        Column() {
            Row() {
                Text('🏠').fontSize(20)
                Text('首页')
                    .fontSize(20)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#333333')
                    .margin({ left: 8 })
            }
            .width('100%')
            .padding({ left: 16, right: 16, top: 12, bottom: 12 })
            .backgroundColor(Color.White)

            Scroll() {
                Column() {
                    Text('🔥 热门活动')
                        .fontSize(14)
                        .fontColor('#666666')
                        .width('100%')
                        .margin({ left: 16, right: 16, top: 16, bottom: 8 })

                    List() {
                        LazyForEach(this.bannerDataSource, (banner: BannerItem) => {
                            ListItem() {
                                this.BannerItemBuilder(banner)
                            }
                        }, (banner: BannerItem) => banner.id)
                    }
                    .listDirection(Axis.Horizontal)
                    .width('100%')
                    .height(160)
                    .padding({ left: 16, right: 16 })
                    .scrollBar(BarState.Off)
                    .edgeEffect(EdgeEffect.Spring)

                    Text('📂 全部分类')
                        .fontSize(14)
                        .fontColor('#666666')
                        .width('100%')
                        .margin({ left: 16, right: 16, top: 20, bottom: 8 })

                    Grid() {
                        LazyForEach(this.categoryDataSource, (category: CategoryItem) => {
                            GridItem() {
                                this.CategoryItemBuilder(category)
                            }
                        }, (category: CategoryItem) => category.id)
                    }
                    .columnsTemplate('1fr 1fr 1fr 1fr')
                    .rowsGap(8)
                    .columnsGap(8)
                    .width('100%')
                    .padding({ left: 16, right: 16 })

                    Text('⭐ 为你推荐')
                        .fontSize(14)
                        .fontColor('#666666')
                        .width('100%')
                        .margin({ left: 16, right: 16, top: 20, bottom: 8 })

                    Grid() {
                        LazyForEach(this.productDataSource, (product: ProductItem) => {
                            GridItem() {
                                this.ProductCardBuilder(product)
                            }
                        }, (product: ProductItem) => product.id)
                    }
                    .columnsTemplate('1fr 1fr')
                    .rowsGap(10)
                    .columnsGap(10)
                    .width('100%')
                    .padding({ left: 16, right: 16, bottom: 20 })
                }
            }
            .scrollBar(BarState.Off)
            .edgeEffect(EdgeEffect.Spring)
            .layoutWeight(1)
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#F5F5F5')
    }

    @Builder
    CategoryPage() {
        Column() {
            Row() {
                Text('📋').fontSize(20)
                Text('分类')
                    .fontSize(20)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#333333')
                    .margin({ left: 8 })
            }
            .width('100%')
            .padding({ left: 16, right: 16, top: 12, bottom: 12 })
            .backgroundColor(Color.White)

            Scroll() {
                Column() {
                    Text('全部分类')
                        .fontSize(14)
                        .fontColor('#666666')
                        .width('100%')
                        .margin({ left: 16, right: 16, top: 16, bottom: 8 })

                    Grid() {
                        LazyForEach(this.categoryDataSource, (category: CategoryItem) => {
                            GridItem() {
                                this.CategoryItemBuilder(category)
                            }
                        }, (category: CategoryItem) => category.id)
                    }
                    .columnsTemplate('1fr 1fr 1fr 1fr')
                    .rowsGap(8)
                    .columnsGap(8)
                    .width('100%')
                    .padding({ left: 16, right: 16 })

                    Text('热门商品')
                        .fontSize(14)
                        .fontColor('#666666')
                        .width('100%')
                        .margin({ left: 16, right: 16, top: 20, bottom: 8 })

                    List() {
                        LazyForEach(this.productDataSource, (product: ProductItem) => {
                            ListItem() {
                                Row() {
                                    Column() {
                                        Text('📦').fontSize(36)
                                    }
                                    .width(100)
                                    .height(100)
                                    .backgroundColor('#F5F5F5')
                                    .borderRadius(8)
                                    .justifyContent(FlexAlign.Center)
                                    .alignItems(HorizontalAlign.Center)
                                    .margin({ right: 12 })

                                    Column() {
                                        Text(product.name)
                                            .fontSize(16)
                                            .fontWeight(FontWeight.Medium)
                                            .fontColor('#333333')
                                            .width('100%')
                                            .margin({ bottom: 6 })

                                        Text(product.description)
                                            .fontSize(12)
                                            .fontColor('#999999')
                                            .maxLines(1)
                                            .width('100%')
                                            .margin({ bottom: 8 })

                                        Row() {
                                            Text(`¥${product.price}`)
                                                .fontSize(18)
                                                .fontColor('#FF4444')
                                                .fontWeight(FontWeight.Bold)
                                            Text(`¥${product.originalPrice}`)
                                                .fontSize(12)
                                                .fontColor('#CCCCCC')
                                                .decoration({ type: TextDecorationType.LineThrough })
                                                .margin({ left: 8 })
                                        }
                                    }
                                    .layoutWeight(1)
                                    .alignItems(HorizontalAlign.Start)
                                }
                                .width('100%')
                                .padding(12)
                                .backgroundColor(Color.White)
                                .borderRadius(8)
                                .margin({ bottom: 10 })
                            }
                        }, (product: ProductItem) => product.id)
                    }
                    .width('100%')
                    .height(320)
                    .padding({ left: 16, right: 16, bottom: 20 })
                    .scrollBar(BarState.Off)
                }
            }
            .scrollBar(BarState.Off)
            .edgeEffect(EdgeEffect.Spring)
            .layoutWeight(1)
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#F5F5F5')
    }

    @Builder
    CartPage() {
        Column() {
            Row() {
                Text('🛒').fontSize(20)
                Text('购物车')
                    .fontSize(20)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#333333')
                    .margin({ left: 8 })
                Blank()
                Text(`${this.cartList.length} 件商品`)
                    .fontSize(14)
                    .fontColor('#666666')
            }
            .width('100%')
            .padding({ left: 16, right: 16, top: 12, bottom: 12 })
            .backgroundColor(Color.White)

            List() {
                LazyForEach(this.cartDataSource, (item: CartItem) => {
                    ListItem() {
                        Row() {
                            Checkbox()
                                .select(item.selected)
                                .onChange((checked: boolean) => {
                                    item.selected = checked;
                                    this.calculateCartTotal();
                                })
                                .width(22)
                                .height(22)
                                .margin({ right: 12 })

                            Column() {
                                Text('📦').fontSize(32)
                            }
                            .width(80)
                            .height(80)
                            .backgroundColor('#F5F5F5')
                            .borderRadius(8)
                            .justifyContent(FlexAlign.Center)
                            .alignItems(HorizontalAlign.Center)
                            .margin({ right: 12 })

                            Column() {
                                Text(item.productName)
                                    .fontSize(15)
                                    .fontColor('#333333')
                                    .width('100%')
                                    .margin({ bottom: 8 })

                                Row() {
                                    Text(`¥${item.price}`)
                                        .fontSize(18)
                                        .fontColor('#FF4444')
                                        .fontWeight(FontWeight.Bold)
                                    Blank()
                                    Row() {
                                        Text('-').fontSize(16).fontColor('#666666')
                                            .padding({ left: 10, right: 10 })
                                            .onClick(() => {
                                                if (item.quantity > 1) {
                                                    item.quantity--;
                                                    this.calculateCartTotal();
                                                }
                                            })
                                        Text(`${item.quantity}`)
                                            .fontSize(14).fontColor('#333333')
                                            .padding({ left: 12, right: 12 })
                                        Text('+').fontSize(16).fontColor('#666666')
                                            .padding({ left: 10, right: 10 })
                                            .onClick(() => {
                                                if (item.quantity < item.maxQuantity) {
                                                    item.quantity++;
                                                    this.calculateCartTotal();
                                                }
                                            })
                                    }
                                    .alignItems(VerticalAlign.Center)
                                    .backgroundColor('#F5F5F5')
                                    .borderRadius(4)
                                }
                                .width('100%')
                                .alignItems(VerticalAlign.Center)
                            }
                            .layoutWeight(1)
                            .alignItems(HorizontalAlign.Start)
                        }
                        .width('100%')
                        .padding(12)
                        .backgroundColor(Color.White)
                        .borderRadius(8)
                        .margin({ bottom: 10 })
                    }
                }, (item: CartItem) => item.id)
            }
            .width('100%')
            .layoutWeight(1)
            .padding({ left: 16, right: 16, top: 10, bottom: 10 })
            .scrollBar(BarState.Off)

            Row() {
                Column() {
                    Text(`已选 ${this.cartSelectedCount}`)
                        .fontSize(12).fontColor('#666666')
                    Text(`合计: ¥${this.cartTotalPrice}`)
                        .fontSize(18).fontColor('#FF4444')
                        .fontWeight(FontWeight.Bold)
                        .margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start)
                Blank()
                Button('去结算')
                    .fontSize(16).fontColor(Color.White)
                    .backgroundColor('#FF4444')
                    .borderRadius(20)
                    .height(40)
                    .padding({ left: 28, right: 28 })
            }
            .width('100%')
            .padding({ left: 16, right: 16, top: 12, bottom: 12 })
            .backgroundColor(Color.White)
            .alignItems(VerticalAlign.Center)
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#F5F5F5')
    }

    @Builder
    MinePage() {
        Column() {
            Column() {
                Row() {
                    Column() {
                        Text('👤').fontSize(40)
                    }
                    .width(80).height(80)
                    .backgroundColor(Color.White)
                    .borderRadius(40)
                    .justifyContent(FlexAlign.Center)
                    .alignItems(HorizontalAlign.Center)
                    .margin({ right: 16 })

                    Column() {
                        Text('用户昵称')
                            .fontSize(20)
                            .fontWeight(FontWeight.Bold)
                            .fontColor(Color.White)
                            .margin({ bottom: 4 })
                        Text('ID: 1008611')
                            .fontSize(14)
                            .fontColor(Color.White)
                            .opacity(0.8)
                    }
                    .alignItems(HorizontalAlign.Start)
                }
                .width('100%')
                .padding(20)
                .alignItems(VerticalAlign.Center)
            }
            .width('100%')
            .backgroundColor('#FF4444')

            Scroll() {
                Column() {
                    Text('我的订单')
                        .fontSize(14).fontColor('#666666')
                        .width('100%')
                        .margin({ left: 16, right: 16, top: 16, bottom: 8 })

                    Row() {
                        Column() {
                            Text('💰').fontSize(24)
                            Text('待付款')
                                .fontSize(12).fontColor('#666666')
                                .margin({ top: 4 })
                        }
                        .layoutWeight(1).alignItems(HorizontalAlign.Center)
                        Column() {
                            Text('📦').fontSize(24)
                            Text('待发货')
                                .fontSize(12).fontColor('#666666')
                                .margin({ top: 4 })
                        }
                        .layoutWeight(1).alignItems(HorizontalAlign.Center)
                        Column() {
                            Text('🚚').fontSize(24)
                            Text('待收货')
                                .fontSize(12).fontColor('#666666')
                                .margin({ top: 4 })
                        }
                        .layoutWeight(1).alignItems(HorizontalAlign.Center)
                        Column() {
                            Text('⭐').fontSize(24)
                            Text('待评价')
                                .fontSize(12).fontColor('#666666')
                                .margin({ top: 4 })
                        }
                        .layoutWeight(1).alignItems(HorizontalAlign.Center)
                    }
                    .width('100%')
                    .padding(16)
                    .backgroundColor(Color.White)
                    .borderRadius(8)
                    .margin({ left: 16, right: 16 })

                    Text('常用功能')
                        .fontSize(14).fontColor('#666666')
                        .width('100%')
                        .margin({ left: 16, right: 16, top: 20, bottom: 8 })

                    List() {
                        ListItem() {
                            Row() {
                                Text('📍').fontSize(20).margin({ right: 12 })
                                Text('收货地址').fontSize(15).fontColor('#333333')
                                Blank()
                                Text('>').fontSize(16).fontColor('#CCCCCC')
                            }
                            .width('100%').padding(16)
                            .backgroundColor(Color.White).borderRadius(8)
                        }
                        ListItem() {
                            Row() {
                                Text('💳').fontSize(20).margin({ right: 12 })
                                Text('支付管理').fontSize(15).fontColor('#333333')
                                Blank()
                                Text('>').fontSize(16).fontColor('#CCCCCC')
                            }
                            .width('100%').padding(16)
                            .backgroundColor(Color.White).borderRadius(8)
                        }
                        ListItem() {
                            Row() {
                                Text('🎁').fontSize(20).margin({ right: 12 })
                                Text('我的优惠').fontSize(15).fontColor('#333333')
                                Blank()
                                Text('>').fontSize(16).fontColor('#CCCCCC')
                            }
                            .width('100%').padding(16)
                            .backgroundColor(Color.White).borderRadius(8)
                        }
                        ListItem() {
                            Row() {
                                Text('⚙️').fontSize(20).margin({ right: 12 })
                                Text('设置').fontSize(15).fontColor('#333333')
                                Blank()
                                Text('>').fontSize(16).fontColor('#CCCCCC')
                            }
                            .width('100%').padding(16)
                            .backgroundColor(Color.White).borderRadius(8)
                        }
                    }
                    .width('100%')
                    .height(280)
                    .padding({ left: 16, right: 16, bottom: 20 })
                    .scrollBar(BarState.Off)
                }
            }
            .scrollBar(BarState.Off)
            .layoutWeight(1)
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#F5F5F5')
    }

    build() {
        Column() {
            Tabs({ index: this.currentTabIndex }) {
                TabContent() {
                    this.HomePage()
                }
                .tabBar('首页')

                TabContent() {
                    this.CategoryPage()
                }
                .tabBar('分类')

                TabContent() {
                    this.CartPage()
                }
                .tabBar('购物车')

                TabContent() {
                    this.MinePage()
                }
                .tabBar('我的')
            }
            .width('100%')
            .layoutWeight(1)
            .onChange((index: number) => {
                this.currentTabIndex = index;
            })
            .barHeight(56)
            .barBackgroundColor(Color.White)
            .animationDuration(300)
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#F5F5F5')
    }
}

五、API 24 新特性应用

5.1 Tabs 嵌套滚动支持

在 API 24 中,Tabs 的 TabContent 内部包含可滚动组件时,滚动体验更加流畅。

Scroll() {
    Column() {
        // 内容
    }
}
.nestedScroll({
    scrollForward: NestedScrollMode.PARENT_FIRST,
    scrollBackward: NestedScrollMode.SELF_FIRST
})

5.2 动态布局容器

API 24 允许在运行时切换布局算法,而不需要重建组件:

@State layoutMode: number = 0;

switchLayout() {
    this.layoutMode = (this.layoutMode + 1) % 3;
}

// 使用不同的布局算法
// GridLayoutAlgorithm - 网格布局
// ListLayoutAlgorithm - 列表布局
// WaterFallLayoutAlgorithm - 瀑布流布局

5.3 性能优化

API 24 提供了更多性能监控工具:

// 监控组件渲染耗时
aboutToAppear() {
    console.info('Component render start');
}

aboutToDisappear() {
    console.info('Component render end');
}

六、性能优化与最佳实践

6.1 LazyForEach 使用规范

  1. 必须使用唯一 Key:每个数据项的 key 必须唯一且稳定
  2. 避免在 Builder 中修改数据:数据变更应在 DataSource 中进行
  3. 合理设置缓存数量:使用 cachedCount 预加载邻近数据
List() {
    LazyForEach(this.dataSource, (item: Item) => {
        ListItem() { /* 内容 */ }
    }, (item: Item) => item.id)  // 唯一 Key
}
.cachedCount(3)  // 缓存邻近 3 项

6.2 组件渲染优化

  1. 使用 @Builder 抽取公共组件:减少重复代码,提高可维护性
  2. 控制组件层级:过深的组件层级会影响渲染性能
  3. 避免过度渲染:合理使用 @State 和 @Prop

6.3 内存管理

  1. 及时注销监听器:避免内存泄漏
  2. 合理释放资源:在 aboutToDisappear 中释放资源
  3. 避免大对象引用:及时清理不再使用的大对象

七、常见问题与解决方案

7.1 LazyForEach 不更新

问题:调用 notifyDataChange 后界面没有更新。

解决方案

  • 检查 notifyDataChange 的参数是否正确
  • 确认数据源中的数据确实已更新
  • 检查 Builder 方法是否正确引用了数据

7.2 Tabs 切换闪烁

问题:切换 Tab 时出现闪烁现象。

解决方案

  • 使用 animationDuration 设置合理的动画时长
  • 确保 TabContent 中的组件没有耗时操作
  • 考虑使用懒加载方式初始化 Tab

7.3 List 滚动卡顿

问题:List 滚动时出现卡顿。

解决方案

  • 使用 LazyForEach 替代 ForEach
  • 减少每个 ListItem 的复杂度
  • 使用 cachedCount 预加载邻近数据
  • 检查是否有过多的日志输出

八、总结与展望

本文详细介绍了如何使用 Tabs + List + Grid 组合构建复杂的电商首页。通过实际案例,我们学习了:

  1. 组件基础:Tabs、List、Grid 的核心用法
  2. 懒加载机制:LazyForEach 与 IDataSource 的配合
  3. 实战开发:完整的电商首页实现
  4. API 24 新特性:嵌套滚动、动态布局等
  5. 性能优化:最佳实践和注意事项

8.1 核心要点回顾

要点 说明
Tabs 容器 用于页面级导航,支持多种配置
List 组件 线性排列,支持横向/纵向滚动
Grid 组件 网格排列,灵活的 columnsTemplate
LazyForEach 按需渲染,性能优异
IDataSource 数据源接口,必须正确实现
@Builder 组件抽取,代码复用

8.2 进阶方向

  • 动画效果:结合 animateTo 实现流畅动画
  • 网络请求:整合 HTTP Kit 获取真实数据
  • 本地存储:使用 Preferences 缓存数据
  • 多设备适配:响应式布局适配不同屏幕
  • 深色模式:支持系统深色模式
  • 无障碍:添加无障碍属性支持

8.3 展望

随着 HarmonyOS 的不断发展,UI 开发将变得更加强大和便捷。未来可以期待:

  • 更智能的布局系统
  • 更强大的动画能力
  • 更好的跨设备体验
  • 更完善的开发工具

附录

A. 完整项目结构

entry/src/main/ets/
├── pages/
│   ├── Index.ets          # 主页面
│   └── DataModels.ets     # 数据模型
├── resources/
│   ├── base/
│   │   ├── element/       # 元素资源
│   │   └── media/         # 图片资源
│   └── en_US/             # 英文资源
│       └── element/
└── module.json5           # 模块配置

B. 运行环境

  • DevEco Studio: 6.1.1 Beta1 或更高版本
  • HarmonyOS SDK: 6.1.1 (API 24)
  • 运行设备: HarmonyOS 手机或模拟器

C. 参考资料


写在最后

感谢阅读本文!希望这份实战指南能够帮助你掌握 HarmonyOS ArkUI 的复杂布局开发。如果有任何问题或建议,欢迎留言交流。让我们一起探索鸿蒙生态的无限可能!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐