Logo

鸿蒙NEXT开发 - 商城 (一)

avatar seven 16 Aug 2024

前言

在本文中,我将通过开发一个简单的商城Demo,记录并分享鸿蒙NEXT开发的经验。本文中使用了以下关键技术点:Navigation、Tab、Swiper、List、Image、WebView、PhotoViewPicker、Toast。希望这篇文章能帮助你更好地理解和应用这些技术点。

开发环境搭建

  1. 安装DevEco Studio开发工具 鸿蒙官方文档
  2. 通过DevEco Studio 创建鸿蒙NEXT项目
  3. 学习一下ArkTS声明式UI 语法

开发实战

官方有两种路由容器组件, 一种是Router, 另一种是Navigation。Router官方已经不推荐使用了,所以我们选择使用Navigation。

创建Navigation

使用DevEco Studio创建好project后, UI入口文件路径是src/main/ets/pages/Index.ets, 在build()函数下定义Navigation作为app的根容器。我们先import MainWebView 两个页面, 这两个页面是我们自己创建的 , 后面再讲怎么创建。创建一个NavPathStackNavPathStack 可以简单理解为Navigation的控制器对象, 我们可以用它来进行页面跳转。PageMap(name: string)方法是定义了页面的名字映射,根据不同的name渲染不同的页面。

importMain from'./main/Index';
importWebView from'./webView/Index';

@Entry
@Component
struct Index {
  pathStack: NavPathStack = newNavPathStack(); // 定义NavPathStack

// 页面生命周期,会在创建自定义组件后,执行其build()函数之前执行(NavDestination创建之前)
aboutToAppear(): void {
    // 将创建的NavPathStack设置到全局AppStorage中, 方便其他页面获取到这个pathStack对象
AppStorage.setOrCreate("PathStack", this.pathStack)
  }

  // 定义页面映射,根据不同的name渲染不同的页面
@Builder
  PageMap(name: string) {
    if(name === "WebView") {
      WebView() // WebView页面
}
  }

  build() {
    Navigation(this.pathStack) {
      Main() // 子节点设为Main页面,默认显示Main页面
}
    .mode(NavigationMode.Stack)
    .navDestination(this.PageMap)
    .hideTitleBar(true)
  }
}

实现底部导航 Tab

现在我们在Main页面创建首页、购物车、我的,三个Tab。

importHome from'./home/Index';
importCart from'./cart/Index';
importMine from'./mine/Index';

@Component
exportdefault struct Index {
  build() {
    NavDestination() {
      Tabs({ barPosition: BarPosition.End }) {
        TabContent() {
          Home()
        }
        .tabBar('首页')

        TabContent() {
          Cart()
        }
        .tabBar('购物车')

        TabContent() {
          Mine()
        }
        .tabBar('我的')
      }
      .width('100%')
      .height('100%')
    }
    .title('Main')
    .hideTitleBar(true)
  }
}

首页:显示轮播图和商品列表

先定义和填充轮播图与商品列表的数据,然后展示出来。

@State swiperData: SwiperItemInterface[] = [];  // 轮播图数据

@State productListData: ProductInterface[] = []; // 商品列表数据数据

aboutToAppear(): void {
  // 填充轮播图假数据
this.swiperData = [
    { id: 1, image: 'https://gips3.baidu.com/it/u=1022347589,1106887837&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280', link: 'https://www.huawei.com' },
    { id: 2, image: 'https://gips3.baidu.com/it/u=764883555,2569275522&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280', link: 'https://www.qq.com' },
    { id: 3, image: 'https://gips1.baidu.com/it/u=1024042145,2716310167&fm=3028&app=3028&f=JPEG&fmt=auto?w=1440&h=2560', link: 'https://www.harmonyos.com/' },
  ];

  // 填充商品列表假数据
constproductListData: ProductInterface[] = [];
  for(letindex = 0; index < 100; index++) {
    constproduct: ProductInterface = {
      id: index + 1,
      title: `冰镇麒麟西瓜单粒约5.5kg ${index + 1}`,
      desc: '单果4.5-5.5kg,不足5.5kg退差价。横切面籽粒连片状黄点,正常现象,不影响食用',
      image: 'https://inews.gtimg.com/newsapp_bt/0/14993002202/1000',
      price: 99.99
    };
    productListData.push(product)
  }
  this.productListData = productListData;
}

然后在build()函数下编写UI代码,这里使用List组件,用ListItemGroup展示Header,这里的Header就是轮播图,用ForEach遍历商品数据,将ListItem添加到List组件内。我直接贴一下完整代码

import{ ProductInterface, SwiperItemInterface } from'ets/interfaces/Index'
import{ promptAction } from'@kit.ArkUI';

@Component
exportdefault struct Index {
  pathStack: NavPathStack = AppStorage.get("PathStack") asNavPathStack // 全局的 NavPathStack

@State swiperData: SwiperItemInterface[] = [];  // 轮播图数据

@State productListData: ProductInterface[] = []; // 商品列表数据数据

aboutToAppear(): void {
    // 填充轮播图假数据
this.swiperData = [
      { id: 1, image: 'https://gips3.baidu.com/it/u=1022347589,1106887837&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280', link: 'https://www.huawei.com' },
      { id: 2, image: 'https://gips3.baidu.com/it/u=764883555,2569275522&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280', link: 'https://www.qq.com' },
      { id: 3, image: 'https://gips1.baidu.com/it/u=1024042145,2716310167&fm=3028&app=3028&f=JPEG&fmt=auto?w=1440&h=2560', link: 'https://www.harmonyos.com/' },
    ];

    // 填充商品列表假数据
constproductListData: ProductInterface[] = [];
    for(letindex = 0; index < 100; index++) {
      constproduct: ProductInterface = {
        id: index + 1,
        title: `冰镇麒麟西瓜单粒约5.5kg ${index + 1}`,
        desc: '单果4.5-5.5kg,不足5.5kg退差价。横切面籽粒连片状黄点,正常现象,不影响食用',
        image: 'https://inews.gtimg.com/newsapp_bt/0/14993002202/1000',
        price: 99.99
      };
      productListData.push(product)
    }
    this.productListData = productListData;
  }

  addProductToCart(product: ProductInterface) {
    constcartData: ProductInterface[] = AppStorage.get('CartData') asProductInterface[] || [];
    cartData.push(product);
    AppStorage.setOrCreate('CartData', cartData);
    promptAction.showToast({ message: '加入购物车成功' });
  }

  @Builder header() {
    // 列表头部组件
Swiper() {
      // 遍历swiperData, 将子节点填充至Swiper
ForEach(this.swiperData, (item: SwiperItemInterface) => {
        Image(item.image)
          .width('100%')
          .height('100%')
          .onClick(() => {
            constparams: Record<string, string> = { 'url': item.link };
            this.pathStack.pushPathByName('WebView', params);
          })
      }, (item: SwiperItemInterface) => String(item.id)) // 设置循环的key
}
    .width('100%')
    .height('35%')
    .loop(true)
    .autoPlay(true)
  }

  build() {
    List() {
      ListItemGroup({
        header: this.header
      })
      // 遍历productListData, 将子节点填充至List
ForEach(this.productListData, (product: ProductInterface) => {
        ListItem() {
          Column() {
            Image(product.image)
              .width('100%')
              .height(200)
              .objectFit(ImageFit.Cover)
              .margin({ bottom: 10 })

            Column() {
              Text(product.title)
                .fontSize(14)
                .fontWeight('bold')
                .maxLines(2)
                .margin({ bottom: 5 })
              Text(product.desc)
                .fontSize(10)
                .maxLines(2)
                .margin({ bottom: 5 })
              Row() {
                Text(`¥ ${product.price}`)
                  .fontSize(14)
                  .fontColor('#FF6331')
                  .layoutWeight(1)
                Image($r('app.media.icon_add_to_cart'))
                  .width(28)
                  .height(28)
                  .onClick(() => this.addProductToCart(product))
              }
              .alignItems(VerticalAlign.Center)
            }
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 5, right: 5 })
          }
          .margin(5)
          .clip(true)
          .padding({ bottom: 10 })
          .backgroundColor('#fff')
          .borderRadius(8)
        }
      }, (product: ProductInterface) => String(product.id)) // 设置循环的key
}
    .layoutWeight(1)
    .lanes(2)
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F6F7')
  }
}

下面我们来看看效果

点击轮播图跳转至WebView页面

我们给Swiper的Image组件添加一个onClick事件,点击图片打开WebView并展示相应的网页。

我们之前在AppStorage存了一个PathStack对象,现在把它取出来,用它来做页面跳转

pathStack: NavPathStack = AppStorage.get("PathStack") asNavPathStack // 全局的 NavPathStack

使用 this.pathStack.pushPathByName('WebView', params)跳转至WebView页面

@Builder header() {
  // 列表头部组件
Swiper() {
    // 遍历swiperData, 将子节点填充至Swiper
ForEach(this.swiperData, (item: SwiperItemInterface) => {
      Image(item.image)
        .width('100%')
        .height('100%')
        .onClick(() => {
           // 跳转至WebView页面,并传入url链接
          constparams: Record<string, string> = { 'url': item.link };
          this.pathStack.pushPathByName('WebView', params);
        })
    }, (item: SwiperItemInterface) => String(item.id)) // 设置循环的key
}
  .width('100%')
  .height('35%')
  .loop(true)
  .autoPlay(true)
}

接下来我们来看看WebView页面的代码, 先定义一个WebviewController, 在aboutToAppear生命周期获取上一个页面传入的参数。这里的 this.pathStack.getParamByName('WebView')获取的是路由栈内所有WebView页面的参数,比如现在已打开了一个WebView页面展示华为官网, 现在要再打开一个WebView页面展示腾讯官网,这时 this.pathStack.getParamByName('WebView')返回的参数url同时包含华为官网和腾讯官网,需要自己取数组最后一个url, 也就是最近的传入的url参数。我觉得这样的API设计挺不合理的,而且也不太健壮,希望后续官方能优化。

import{ webview } from'@kit.ArkWeb';

@Component
exportdefault struct Index {
    pathStack: NavPathStack = AppStorage.get("PathStack") asNavPathStack
    controller: webview.WebviewController = newwebview.WebviewController();
    url: string = '';


    aboutToAppear(): void {
        constallParams: Record<string, string>[] = this.pathStack.getParamByName('WebView') asRecord<string, string>[];
        this.url = allParams[allParams.length - 1]?.url;
        console.log('url is', this.url)
    }

    build() {
        NavDestination() {
            Web({ src: this.url, controller: this.controller })
                .width('100%')
                .height('100%')
        }
    }
}

加入商品到购物车,并弹出加入成功Toast

点击购物车Icon,添加商品到购物车并弹出成功提示。

import{ promptAction } from'@kit.ArkUI';

....
addProductToCart(product: ProductInterface) {
  constcartData: ProductInterface[] = AppStorage.get('CartData') asProductInterface[] || [];
  cartData.push(product);
  AppStorage.setOrCreate('CartData', cartData);
  promptAction.showToast({ message: '加入购物车成功' });
}

选用户头像并展示

使用PhotoViewPicker选择用户头像并展示。

import{ photoAccessHelper } from'@kit.MediaLibraryKit';

@Component
exportdefault struct Index {
  @State avatar: string = 'https://q0.itc.cn/q_70/images01/20240125/3dc71da769524488be1c3e095c0e6e85.png'

  pickAvatar() {
    constphotoSelectOptions = newphotoAccessHelper.PhotoSelectOptions();
    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE
photoSelectOptions.maxSelectNumber = 1; // 选择媒体文件的最大数目
constphotoViewPicker = newphotoAccessHelper.PhotoViewPicker();

    photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
      consturis = photoSelectResult.photoUris;
      this.avatar = uris?.[0];
    })
  }

  build() {
    Column() {
      Image(this.avatar)
        .width(60)
        .height(60)
        .borderRadius(30)
        .margin({ bottom: 10 })
        .onClick(() => this.pickAvatar())
      Text('用户名: 鸿蒙开发者')
        .fontSize(12)
        .fontWeight('bold')
    }
  }
}

总结

本文不仅展示了鸿蒙NEXT的基本开发过程,还分享了关键技术的应用方法。希望本文对你的开发有所帮助。未来我将继续完善这个商城Demo,并分享更多开发经验。

Tags
HarmonyOS
鸿蒙