前言

在前端开发中,首页的视觉效果和用户体验至关重要。今天我们将通过HarmonyOS ArkTS框架,实现一个完整的首页核心组件:轮播图、搜索框和导航指示器。这些组件不仅美观实用,还能显著提升用户交互体验。

一、项目架构设计

首先,我们规划项目结构:
MyHarmonyHome/
├── entry/src/main/ets/
│ ├── components/ # 自定义组件
│ │ ├── BannerSwiper.ets # 轮播图组件
│ │ ├── SearchBar.ets # 搜索框组件
│ │ └── NavIndicator.ets # 导航指示器
│ ├── model/ # 数据模型
│ │ └── HomeModel.ets
│ └── pages/ # 页面
│ └── HomePage.ets
└── resources/ # 资源文件

二、数据模型设计

HomeModel.ets

// 轮播图数据模型
export class BannerItem {
  id: number;
  imageRes: Resource;
  title: string;
  description: string;

  constructor(id: number, imageRes: Resource, title: string, description: string) {
    this.id = id;
    this.imageRes = imageRes;
    this.title = title;
    this.description = description;
  }
}

// 导航项数据模型
export class NavItem {
  id: number;
  icon: Resource;
  title: string;
  active: boolean = false;

  constructor(id: number, icon: Resource, title: string) {
    this.id = id;
    this.icon = icon;
    this.title = title;
  }
}

// 数据源
export class HomeDataSource {
  // 获取轮播图数据
  static getBannerList(): BannerItem[] {
    return [
      new BannerItem(1, $r('app.media.banner1'), '新品上市', '最新科技产品抢先体验'),
      new BannerItem(2, $r('app.media.banner2'), '限时优惠', '全场满减,最高减500元'),
      new BannerItem(3, $r('app.media.banner3'), '热门推荐', '用户口碑爆款推荐'),
      new BannerItem(4, $r('app.media.banner4'), '品质生活', '打造精致生活方式')
    ];
  }

  // 获取导航数据
  static getNavItems(): NavItem[] {
    return [
      new NavItem(1, $r('app.media.icon_home'), '首页'),
      new NavItem(2, $r('app.media.icon_category'), '分类'),
      new NavItem(3, $r('app.media.icon_cart'), '购物车'),
      new NavItem(4, $r('app.media.icon_promo'), '促销'),
      new NavItem(5, $r('app.media.icon_mine'), '我的')
    ];
  }
}

三、轮播图组件实现

BannerSwiper.ets

@Component
export struct BannerSwiper {
  private bannerList: BannerItem[];
  @State currentIndex: number = 0;
  private timerId: number = 0;
  @State imageLoaded: boolean[] = [];

  // 构造函数
  constructor(bannerList: BannerItem[]) {
    this.bannerList = bannerList;
    this.imageLoaded = new Array(bannerList.length).fill(false);
  }

  aboutToAppear() {
    this.startAutoPlay();
  }

  aboutToDisappear() {
    this.stopAutoPlay();
  }

  // 自动播放控制
  private startAutoPlay() {
    if (this.timerId) clearInterval(this.timerId);
    if (this.bannerList.length <= 1) return;
    
    this.timerId = setInterval(() => {
      let nextIndex = this.currentIndex + 1;
      if (nextIndex >= this.bannerList.length) {
        nextIndex = 0;
      }
      this.currentIndex = nextIndex;
    }, 3500);
  }

  private stopAutoPlay() {
    if (this.timerId) {
      clearInterval(this.timerId);
      this.timerId = 0;
    }
  }

  // 构建轮播项
  @Builder
  private BannerItemBuilder(item: BannerItem, index: number) {
    Stack({ alignContent: Alignment.BottomStart }) {
      // 图片区域
      Image(item.imageRes)
        .width('100%')
        .height(200)
        .objectFit(ImageFit.Cover)
        .borderRadius(16)
        .transition({ type: TransitionType.Insert, opacity: 0 })
        .transition({ type: TransitionType.Delete, opacity: 0 })

      // 标题遮罩层
      Column() {
        Text(item.title)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .margin({ bottom: 4 })
        
        Text(item.description)
          .fontSize(12)
          .fontColor(Color.White)
          .opacity(0.9)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12, top: 12 })
      .backgroundBlur(10)
      .backgroundColor('rgba(0,0,0,0.4)')
      .borderRadius({
        bottomLeft: 16,
        bottomRight: 16
      })
    }
    .width('100%')
    .height(200)
    .shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 4 })
  }

  // 构建指示器
  @Builder
  private IndicatorBuilder() {
    Row({ space: 6 }) {
      ForEach(this.bannerList, (item: BannerItem, index: number) => {
        Stack() {
          // 背景圆
          Circle()
            .width(32)
            .height(8)
            .fill(this.currentIndex === index ? '#FF6A00' : '#E0E0E0')
            .opacity(this.currentIndex === index ? 0.3 : 0.2)
          
          // 前景圆点
          Circle()
            .width(this.currentIndex === index ? 16 : 8)
            .height(8)
            .fill(this.currentIndex === index ? '#FF6A00' : '#666666')
            .animation({
              duration: 300,
              curve: Curve.EaseOut
            })
        }
        .onClick(() => {
          this.currentIndex = index;
          this.startAutoPlay();
        })
      })
    }
    .margin({ top: 12 })
    .justifyContent(FlexAlign.Center)
  }

  build() {
    Column() {
      // Swiper轮播图
      Swiper() {
        ForEach(this.bannerList, (item: BannerItem, index: number) => {
          this.BannerItemBuilder(item, index)
        })
      }
      .width('100%')
      .height(200)
      .indicator(false)
      .loop(true)
      .autoPlay(true)
      .interval(3500)
      .duration(500)
      .curve(Curve.EaseInOut)
      .onChange((index: number) => {
        this.currentIndex = index;
      })
      .onTouch((event: TouchEvent) => {
        if (event.type === TouchType.Down) {
          this.stopAutoPlay();
        } else if (event.type === TouchType.Up) {
          this.startAutoPlay();
        }
      })

      // 自定义指示器
      this.IndicatorBuilder()
    }
    .width('100%')
    .padding({ left: 16, right: 16 })
  }
}

四、搜索框组件实现

SearchBar.ets

@Component
export struct SearchBar {
  @State searchText: string = '';
  @State isFocused: boolean = false;
  private placeholder: string = '搜索商品或品牌';
  
  // 搜索回调函数
  private onSearch: (text: string) => void = () => {};

  constructor(placeholder?: string, onSearch?: (text: string) => void) {
    if (placeholder) this.placeholder = placeholder;
    if (onSearch) this.onSearch = onSearch;
  }

  // 执行搜索
  private performSearch() {
    if (this.searchText.trim().length > 0) {
      this.onSearch(this.searchText);
      // 收起键盘
      this.isFocused = false;
    }
  }

  // 清空搜索
  private clearSearch() {
    this.searchText = '';
    this.isFocused = false;
  }

  build() {
    Row() {
      // 搜索图标
      Image($r('app.media.icon_search'))
        .width(20)
        .height(20)
        .margin({ left: 12, right: 8 })
        .objectFit(ImageFit.Contain)

      // 输入框
      TextInput({
        text: this.searchText,
        placeholder: this.placeholder
      })
        .width('70%')
        .height(40)
        .fontSize(16)
        .fontColor('#333333')
        .placeholderColor('#999999')
        .caretColor('#FF6A00')
        .maxLength(50)
        .onChange((value: string) => {
          this.searchText = value;
        })
        .onSubmit(() => {
          this.performSearch();
        })
        .onEditChange((isEditing: boolean) => {
          this.isFocused = isEditing;
        })

      // 清除按钮(有内容时显示)
      if (this.searchText.length > 0) {
        Image($r('app.media.icon_clear'))
          .width(16)
          .height(16)
          .margin({ left: 8, right: 8 })
          .onClick(() => {
            this.clearSearch();
          })
      }

      // 搜索按钮
      Button('搜索', { type: ButtonType.Capsule })
        .width(60)
        .height(32)
        .fontSize(14)
        .fontColor(Color.White)
        .backgroundColor('#FF6A00')
        .margin({ left: 8, right: 12 })
        .stateEffect(true)
        .onClick(() => {
          this.performSearch();
        })
    }
    .width('100%')
    .height(56)
    .backgroundColor(Color.White)
    .borderRadius(28)
    .shadow(this.isFocused ? {
      radius: 16,
      color: '#1AFF6A00',
      offsetX: 0,
      offsetY: 4
    } : {
      radius: 8,
      color: '#0A000000',
      offsetX: 0,
      offsetY: 2
    })
    .animation({
      duration: 300,
      curve: Curve.EaseOut
    })
    .onClick(() => {
      if (!this.isFocused) {
        this.isFocused = true;
      }
    })
  }
}

五、导航指示器组件

NavIndicator.ets

@Component
export struct NavIndicator {
  private navItems: NavItem[];
  @State currentIndex: number = 0;
  private onItemClick: (index: number, item: NavItem) => void = () => {};

  constructor(
    navItems: NavItem[],
    initialIndex: number = 0,
    onItemClick?: (index: number, item: NavItem) => void
  ) {
    this.navItems = navItems;
    this.currentIndex = initialIndex;
    if (onItemClick) this.onItemClick = onItemClick;
    
    // 设置初始激活状态
    if (this.navItems.length > 0) {
      this.navItems.forEach((item, index) => {
        item.active = index === initialIndex;
      });
    }
  }

  // 切换导航项
  private switchNav(index: number) {
    if (index < 0 || index >= this.navItems.length) return;
    
    // 更新激活状态
    this.navItems.forEach((item, i) => {
      item.active = i === index;
    });
    
    this.currentIndex = index;
    this.onItemClick(index, this.navItems[index]);
  }

  // 构建导航项
  @Builder
  private NavItemBuilder(item: NavItem, index: number) {
    Column({ space: 4 }) {
      // 图标容器
      Stack() {
        // 背景圆(激活时有动画)
        Circle()
          .width(item.active ? 44 : 0)
          .height(44)
          .fill('#FFF2E8')
          .opacity(item.active ? 1 : 0)
          .animation({
            duration: 300,
            curve: Curve.EaseOut
          })
        
        // 图标
        Image(item.icon)
          .width(24)
          .height(24)
          .objectFit(ImageFit.Contain)
          .fillColor(item.active ? '#FF6A00' : '#666666')
          .animation({
            duration: 200,
            curve: Curve.EaseInOut
          })
      }
      .width(44)
      .height(44)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)

      // 标题
      Text(item.title)
        .fontSize(12)
        .fontColor(item.active ? '#FF6A00' : '#666666')
        .fontWeight(item.active ? FontWeight.Medium : FontWeight.Normal)
        .animation({
          duration: 200,
          curve: Curve.EaseInOut
        })
      
      // 激活指示条
      Rectangle()
        .width(20)
        .height(3)
        .fill('#FF6A00')
        .radius({ topLeft: 2, topRight: 2 })
        .opacity(item.active ? 1 : 0)
        .margin({ top: 2 })
        .animation({
          duration: 300,
          curve: Curve.EaseOut
        })
    }
    .width(64)
    .onClick(() => {
      this.switchNav(index);
    })
  }

  build() {
    Row({ space: 0 }) {
      ForEach(this.navItems, (item: NavItem, index: number) => {
        this.NavItemBuilder(item, index)
      })
    }
    .width('100%')
    .height(72)
    .backgroundColor(Color.White)
    .borderRadius({
      topLeft: 20,
      topRight: 20
    })
    .shadow({
      radius: 16,
      color: '#1A000000',
      offsetX: 0,
      offsetY: -4
    })
    .justifyContent(FlexAlign.SpaceAround)
    .padding({ top: 8, bottom: 12 })
  }
}

六、完整首页集成

HomePage.ets

import { BannerSwiper, SearchBar, NavIndicator } from '../components';
import { HomeDataSource } from '../model/HomeModel';
import prompt from '@ohos.prompt';

@Entry
@Component
struct HomePage {
  // 页面数据
  @State bannerData: BannerItem[] = [];
  @State navData: NavItem[] = [];
  @State currentPage: string = '首页';
  @State searchHistory: string[] = ['鸿蒙手机', '智能手表', '平板电脑'];

  // 页面加载
  aboutToAppear() {
    this.loadHomeData();
  }

  // 加载数据
  loadHomeData() {
    this.bannerData = HomeDataSource.getBannerList();
    this.navData = HomeDataSource.getNavItems();
  }

  // 搜索处理
  handleSearch(searchText: string) {
    if (searchText.trim().length === 0) {
      prompt.showToast({
        message: '请输入搜索关键词',
        duration: 1500
      });
      return;
    }

    prompt.showToast({
      message: `搜索: ${searchText}`,
      duration: 2000
    });

    // 添加到搜索历史(去重)
    if (!this.searchHistory.includes(searchText)) {
      this.searchHistory.unshift(searchText);
      if (this.searchHistory.length > 5) {
        this.searchHistory.pop();
      }
    }

    console.log(`执行搜索: ${searchText}`);
    // 实际开发中这里调用搜索API
  }

  // 导航点击处理
  handleNavClick(index: number, item: NavItem) {
    this.currentPage = item.title;
    prompt.showToast({
      message: `切换到: ${item.title}`,
      duration: 1500
    });

    // 根据导航项执行不同操作
    switch (index) {
      case 0: // 首页
        // 刷新首页数据
        this.loadHomeData();
        break;
      case 1: // 分类
        // 跳转到分类页面
        console.log('跳转到分类页面');
        break;
      case 2: // 购物车
        // 跳转到购物车
        console.log('跳转到购物车');
        break;
      // ... 其他导航项
    }
  }

  build() {
    Column() {
      // 1. 顶部状态栏区域
      Row() {
        Text('HarmonyOS商城')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        
        Image($r('app.media.icon_notify'))
          .width(24)
          .height(24)
          .margin({ left: 'auto' })
          .onClick(() => {
            prompt.showToast({ message: '消息中心', duration: 1500 });
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 8 })

      // 2. 搜索框
      SearchBar('搜索商品或品牌', (text: string) => {
        this.handleSearch(text);
      })
      .margin({ left: 16, right: 16, bottom: 12 })

      // 3. 轮播图
      BannerSwiper({ bannerList: this.bannerData })
      .margin({ bottom: 20 })

      // 4. 快捷入口(可选)
      Text('快捷入口')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ left: 16, bottom: 12 })
        .alignSelf(HorizontalAlign.Start)

      // 5. 占位内容区域
      Scroll() {
        Column() {
          // 这里可以添加其他内容组件
          Text('热门推荐')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin({ top: 20, bottom: 12 })
          
          // 示例商品列表
          ForEach([1, 2, 3, 4], (item: number) => {
            Row() {
              Image($r('app.media.product' + item))
                .width(80)
                .height(80)
                .borderRadius(8)
                .margin({ right: 12 })
              
              Column({ space: 6 }) {
                Text(`商品${item} - HarmonyOS生态产品`)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                Text('¥' + (item * 999))
                  .fontSize(18)
                  .fontColor('#FF6A00')
                  .fontWeight(FontWeight.Bold)
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(12)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .margin({ bottom: 8 })
            .shadow({ radius: 4, color: '#0A000000' })
          })
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 80 })
      }
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%')

      // 6. 底部导航指示器
      NavIndicator(this.navData, 0, (index: number, item: NavItem) => {
        this.handleNavClick(index, item);
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F8F8')
  }
}

七、最终效果展示

运行效果图:
在这里插入图片描述

八、关键技术点

1. 组件化开发
每个功能模块都封装成独立组件,便于复用和维护。

2. 状态管理
使用 @State 装饰器管理组件内部状态,实现数据驱动UI更新。

3. 动画效果
通过 ArkTS 动画API实现平滑的过渡效果,提升用户体验。

4. 事件处理
完善的触摸、点击事件处理,提供良好的交互反馈。

5. 响应式设计
适配不同屏幕尺寸,确保在各种设备上都有良好表现。

九、扩展建议

性能优化:

图片懒加载

虚拟列表

组件复用

功能增强:

搜索联想

轮播图3D效果

导航徽标提示

主题切换:

支持深色模式

自定义主题色

总结
通过今天的实战,我们学习了如何在HarmonyOS中实现首页核心组件。这些组件不仅功能完整,而且具有良好的用户体验和视觉效果。掌握这些技术后,你可以轻松构建出美观实用的HarmonyOS应用界面。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐