【鸿蒙开发】饿了么页面练习

0. 整体结构

整体划分3部分。店铺部分,购物车部分,金额统计部分。使用 Stack把3部分堆叠

0.1 整体页面 Index.ets

修改 Index.ets ,使用堆叠布局,并居底部对齐

javascript 复制代码
import { ElShop } from '../components/ElShop'
import { ElShoppingCart } from '../components/ElShoppingCart'
import { ElSubtotal } from '../components/ElSubtotal'

@Entry
@Component
struct Index {
  build() {
    Column() {
      Stack({ alignContent: Alignment.Bottom }) {
        ElShop()
        ElShoppingCart()
        ElSubtotal()
      }
    }
    .width("100%")
    .height("100%")
  }
}

0.2 创建 ElShop 组件

创建 ElShop 店铺部分组件

javascript 复制代码
@Component
export struct ElShop {
  build() {
    Column() {
    }
    .width("100%")
    .height("100%")
    .backgroundColor(Color.Red)
  }
}

0.3 创建 ElShoppingCart 组件

创建购物车部分组件

javascript 复制代码
@Component
export struct ElShoppingCart {
  build() {
    Column() {
    }
    .width("100%")
    .height(300)
    .backgroundColor(Color.Green)
  }
}

0.4 创建 ElSubtotal 组件

创建金额统计部分组件

javascript 复制代码
@Component
export struct ElSubtotal {
  build() {
    Column() {
    }
    .width("100%")
    .height(80)
    .backgroundColor(Color.Blue)
  }
}

0.5 创建 model

创建 models 文件夹,创建 Product.ets 文件

javascript 复制代码
export class Product {
  id: number = 0
  name: string = ""
  positive_reviews: string = ""
  food_label_list: string[] = []
  price: number = 0
  picture: string = ""
  description: string = ""
  tag: string = ""
  monthly_sales: number = 0
}

export class SelectedProduct extends Product {
  count: number = 0
}

export class Category {
  id: number = 0
  name: string = ""
  foods: Product[] = []
}

1. 店铺部分

1.1 修改 ElShop 组件

划分 header,tabbar,body 三部分

Column [ ElShopHeader,ElShopTabbar,ElShopBody ]

javascript 复制代码
import { ElShopHeader } from './ElShopHeader'
import { ElShopTabbar } from './ElShopTabbar'
import { ElShopBody } from './ElShopBody'

@Component
export struct ElShop {
  build() {
    Column() {
      ElShopHeader()
      ElShopTabbar()
      ElShopBody()
    }
    .width("100%")
    .height("100%")
    .backgroundColor(Color.White)
  }
}

1.2 创建 ElShopHeader 组件

Row [ 返回图标,(搜索图标,文字),消息图标,喜欢图标,加号图标 ]

javascript 复制代码
@Component
export struct ElShopHeader {
  build() {
    Row() {
      Image($r("app.media.left"))
        .width(20)
        .height(20)
        .fillColor("#191919")

      Row() {
        Image($r('app.media.search'))
          .width(14)
          .aspectRatio(1)
          .fillColor('#555')
          .margin({ right: 5 })
        Text('搜一搜')
          .fontSize(12)
          .fontColor('#555')
      }
      .width(150)
      .height(30)
      .backgroundColor('#eee')
      .borderRadius(15)
      .padding({ left: 5, right: 5 })

      Image($r('app.media.message'))
        .width(20)
        .fillColor("#191919")

      Image($r('app.media.favor'))
        .width(20)
        .fillColor("#191919")

      Image($r("app.media.add"))
        .width(20)
        .fillColor("#191919")

    }
    .width('100%')
    .height(60)
    .backgroundColor('#fbfbfb')
    .padding(10)
    .justifyContent(FlexAlign.SpaceAround)
  }
}

1.3 创建 ElShopTabbar 组件

Row [ 点餐,评价,商家 ]

每一个tab用 @Builder 函数创建

javascript 复制代码
@Component
export struct ElShopTabbar {
  @Builder
  TabItem(active: boolean, title: string, subtitle?: string) {
    Column() {
      Text() {
        Span(title)
        if (subtitle) {
          Span(' ' + subtitle)
            .fontSize(10)
            .fontColor(active ? '#000' : '#666')
        }
      }
      .layoutWeight(1)
      .fontColor(active ? '#000' : '#666')
      .fontWeight(active ? FontWeight.Bold : FontWeight.Normal)

      Column()
        .width(20)
        .height(3)
        .borderRadius(5)
        .backgroundColor(active ? '#02B6FD' : 'transparent')
    }
    .alignItems(HorizontalAlign.Center)
    .padding({ left: 15, right: 15 })
  }

  build() {
    Row() {
      this.TabItem(true, '点餐')
      this.TabItem(false, '评价', '196')
      this.TabItem(false, '商家')
    }
    .width("100%")
    .height(40)
    .justifyContent(FlexAlign.Start)
    .backgroundColor('#fbfbfb')
  }
}

1.4 创建 ElShopBody 组件

这里分为左边分类列表,右边商品列表

Row [ 分类列表,商品列表 ]

javascript 复制代码
import { ElShopCategory } from './ElShopCategory'
import { ElShopProduct } from './ElShopProduct'

@Component
export struct ElShopBody {
  build() {
    Row() {
      ElShopCategory()
      ElShopProduct()
    }
    .width('100%')
    .layoutWeight(1)
    .alignItems(VerticalAlign.Top)
  }
}

1.5 创建 ElShopCategory 组件

分类列表,每一项是分类文字

javascript 复制代码
import { Category } from '../models/Product'

@Component
export struct ElShopCategory {
  @State categoryList: Category[] = [
    { id: 1, name: '必点招牌', foods: [] },
    { id: 2, name: '超值套餐', foods: [] },
    { id: 3, name: '杂粮主食', foods: [] },
  ]
  @State categoryIndex: number = 0

  build() {
    Column() {
      ForEach(this.categoryList, (item: Category, index: number) => {
        Text(item.name)
          .width('100%')
          .height(40)
          .textAlign(TextAlign.Center)
          .fontSize(12)
          .backgroundColor(this.categoryIndex === index ? '#fff' : 'transparent')
          .onClick(() => {
            this.categoryIndex = index
          })
      })
    }
    .width(90)
    .height('100%')
    .backgroundColor('#eee')
  }
}

1.6 创建 ElShopProduct 组件

商品列表,每一项是商品项

javascript 复制代码
import { ElProductItem } from './ElProductItem'

@Component
export struct ElShopProduct {
  build() {
    List({ space: 20 }) {
      ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9], () => {
        ListItem() {
          ElProductItem()
        }
      })
    }
    .layoutWeight(1)
    .backgroundColor('#fff')
    .padding({ left: 10, right: 10 })
  }
}

1.7 创建 ElProductItem 组件

商品的每一项

Row [ 图片,内容 ]

javascript 复制代码
@Component
export struct ElProductItem {
  build() {
    Row() {
      Image('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F67ba10b0-b4a0-4dd7-b343-31830e01b616%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1711612969&t=b2102c0d151f8225ba531caadf26dd6f')
        .width(60)
        .aspectRatio(1)
        .borderRadius(8)

      Column({ space: 5 }) {
        Text('猪脚+肉卷+鸡蛋')
          .fontSize(14)
          .textOverflow({
            overflow: TextOverflow.Ellipsis
          })
          .maxLines(1)

        Text('用料:猪脚,肉卷,鸡蛋')
          .fontSize(12)
          .fontColor('#999')
          .textOverflow({
            overflow: TextOverflow.Ellipsis
          })
          .maxLines(1)

        Row() {
          Text() {
            Span('¥ ')
              .fontColor('#FF4B33')
              .fontSize(10)
            Span('38.65')
              .fontColor('#FF4B33')
              .fontWeight(FontWeight.Bold)
          }
          // 商品数量操作
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .justifyContent(FlexAlign.SpaceBetween)
      .padding({ left: 10, right: 10 })
      .height(60)
    }
    .alignItems(VerticalAlign.Top)
  }
}

2. 金额统计部分

2.1 修改 ElSubtotal 组件

Row [ 购物车图标,金额文字,结算按钮 ]

javascript 复制代码
@Component
export struct ElSubtotal {
  build() {
    Row() {
      Badge({
        count: 1,
        position: BadgePosition.RightTop,
        style: { badgeSize: 20 }
      }) {
        Image($r("app.media.shopping_cart_icon"))
      }
      .width(50)
      .height(50)
      .margin({ right: 10 })

      Column() {
        Text() {
          Span('¥')
            .fontSize(14)
          Span('0')
            .fontSize(24)
        }

        Text('另需配送费约 ¥3.3')
          .fontSize(12)
          .fontColor('#999')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      Button('去结算')
        .fontSize(18)
        .backgroundColor('#02B6FD')
        .padding({ left: 30, right: 30 })

    }
    .width('100%')
    .height(80)
    .padding(10)
    .alignItems(VerticalAlign.Center)
    .backgroundColor(Color.White)
    .border({
      color: "#f5f5f5",
      width: {
        top: "1"
      }
    })
  }
}

3. 购物车部分

给购物车内容的外层嵌套一个透明的遮罩

外层遮罩 Column [ Colunm( 标题,已选商品列表 ) ]

3.1 修改 ElShoppingCart 组件

javascript 复制代码
import { ElProductItem } from './ElProductItem'

@Component
export struct ElShoppingCart {
  build() {
    Column() {
      Column() {
        Row() {
          Text('已选商品')
            .fontSize(13)
            .fontWeight(600)
          Row() {
            Image($r("app.media.delete"))
              .height(14)
              .fillColor('#999')
              .margin({ right: 5 })
            Text('清空')
              .fontSize(13)
              .fontColor('#999')
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .padding(15)

        List({ space: 20 }) {
          ForEach([1, 2, 3, 4], () => {
            ListItem() {
              ElProductItem()
            }
          })
        }
        .divider({
          strokeWidth: 1,
          color: '#ddd'
        })
        .padding({ left: 15, right: 15, bottom: 100 })
      }
      .backgroundColor('#fff')
      .borderRadius({
        topLeft: 16,
        topRight: 16
      })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.End)
    .backgroundColor('rgba(0,0,0,0.5)')
  }
}

3.2 修改购物车显示隐藏

当点击底部统计部分才显示购物车部分

修改 Index.ets ,添加 showShoppingCart 属性

javascript 复制代码
@Entry
@Component
struct Index {
  @State showShoppingCart: boolean = false

  build() {
    Column() {
      Stack({ alignContent: Alignment.Bottom }) {
        ElShop()
        if (this.showShoppingCart) {
          ElShoppingCart()
        }
        ElSubtotal({ showShoppingCart: $showShoppingCart })
      }
    }
    .width("100%")
    .height("100%")
  }
}

修改 ElSubtotal 金额统计部分组件,接受 showShoppingCart 属性

javascript 复制代码
@Link showShoppingCart: boolean

修改 ElSubtotal 组件,添加点击事件修改 showShoppingCart 值

javascript 复制代码
.onClick(() => {
  this.showShoppingCart = !this.showShoppingCart
})

4. 渲染商品数据

4.1 安装 live-server

使用 npm 全局安装 live-server 包

javascript 复制代码
npm i live-server -g

在 elshop.json 文件夹启动 live-server

javascript 复制代码
live-server

4.2 安装 axios

在项目中安装 axios

javascript 复制代码
ohpm install @ohos/axios

4.3 获取 elshop.json 数据

修改 Index.ets,获取json数据

javascript 复制代码
@Entry
@Component
struct Index {
  @State showShoppingCart: boolean = false
  @Provide categoryList: Category[] = []
  @Provide categoryIndex: number = 0

  aboutToAppear() {
    this.getData()
  }

  async getData() {
    const res = await axios.get("http://127.0.0.1:8080/elshop.json")
    const category = res.data.category.map(item => {
      const foods = item.foods.map(food => {
        return { ...food, count: 0 }
      })
      return { ...item, foods }
    })
    this.categoryList = category
  }
}

4.4 修改 ElShopCategory 组件

修改从祖先组件获取分类数据

javascript 复制代码
@Component
export struct ElShopCategory {
  @Consume categoryIndex: number
  @Consume categoryList: Category[]
}

4.5 修改 ElShopProduct 组件

修改从祖先组件获取分类数据,循环分类下的商品,并把 product 传给 ElProductItem 组件

javascript 复制代码
import { Category, SelectedProduct } from '../models/Product'
import { ElProductItem } from './ElProductItem'

@Component
export struct ElShopProduct {
  @Consume categoryIndex: number
  @Consume categoryList: Category[]

  build() {
    List({ space: 20 }) {
      ForEach(this.categoryList[this.categoryIndex]?.foods ?? [], (product: SelectedProduct) => {
        ListItem() {
          ElProductItem({ product })
        }
      })
    }
    .layoutWeight(1)
    .backgroundColor('#fff')
    .padding({ left: 10, right: 10 })
  }
}

4.6 修改 ElProductItem 组件

修改 ElProductItem 组件,接收 product 数据

javascript 复制代码
import { SelectedProduct } from '../models/Product'

@Component
export struct ElProductItem {
  product: SelectedProduct

  build() {
    Row() {
      Image(this.product.picture)
        .width(60)
        .aspectRatio(1)
        .borderRadius(8)

      Column({ space: 5 }) {
        Text(this.product.name)
          .fontSize(14)
          .textOverflow({
            overflow: TextOverflow.Ellipsis
          })
          .maxLines(1)

        Text(this.product.description)
          .fontSize(12)
          .fontColor('#999')
          .textOverflow({
            overflow: TextOverflow.Ellipsis
          })
          .maxLines(1)

        Row() {
          Text() {
            Span('¥ ')
              .fontColor('#FF4B33')
              .fontSize(10)
            Span(this.product.price.toString())
              .fontColor('#FF4B33')
              .fontWeight(FontWeight.Bold)
          }
          // 商品数量操作
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .justifyContent(FlexAlign.SpaceBetween)
      .padding({ left: 10, right: 10 })
      .height(60)
    }
    .alignItems(VerticalAlign.Top)
  }
}

5. 商品数量操作

5.1 创建 utils/productUtil.ets 文件

为了持久化保存已选择的商品,把选中的商品保存到 AppStorage 中

  • 声明保存到 AppStoreage 的 key
  • 添加已选的商品 addProduct
  • 删除已选的商品 removeProduct
  • 清空已选的商品 cleartAllProduct
javascript 复制代码
import { Product, SelectedProduct } from '../models/Product'

export const SHOPPING_CART_KEY = "SHOPPING_CART"

// 添加商品
export const addProduct = (product: Product) => {
  const products = JSON.parse(AppStorage.Get<string>(SHOPPING_CART_KEY) || '[]') as SelectedProduct[]
  const selectedProduct = products.find(item => item.id === product.id)
  if (selectedProduct) {
    selectedProduct.count++
  } else {
    products.push({ ...product, count: 1 })
  }
  AppStorage.Set<string>(SHOPPING_CART_KEY, JSON.stringify(products))
}

// 删除商品
export const removeProduct = (id: number) => {
  const products = JSON.parse(AppStorage.Get<string>(SHOPPING_CART_KEY) || '[]') as SelectedProduct[]
  const index = products.findIndex(item => item.id === id)
  const selectedProduct = products[index]
  if (selectedProduct && selectedProduct.count > 0) {
    selectedProduct.count--
    if (selectedProduct.count <= 0) {
      products.splice(index, 1)
    }
  }
  AppStorage.Set<string>(SHOPPING_CART_KEY, JSON.stringify(products))
}

// 清空商品
export const clearAllProduct = () => {
  AppStorage.Set<string>(SHOPPING_CART_KEY, "[]")
}

5.2 修改 Index.ets 文件

在 Index.ets 页面初始化持久化的数据

javascript 复制代码
import { SHOPPING_CART_KEY } from '../utils/productUtil'

PersistentStorage.PersistProp(SHOPPING_CART_KEY,"[]")

添加持久化的json数据属性,并监听更新变化

javascript 复制代码
  @StorageLink(SHOPPING_CART_KEY)
  @Watch("update")
  productListJson: string = "[]"
  @Provide selectedProductList: SelectedProduct[] = []

  update() {
    this.selectedProductList = JSON.parse(this.productListJson)
  }

5.3 修改 ElShoppingCart 组件

接收已选中商品数据 selectedProductList

javascript 复制代码
export struct ElShoppingCart {
  @Consume selectedProductList: SelectedProduct[]
}

并修改列表渲染,把 product 传给 ElProductItem 组件

javascript 复制代码
List({ space: 20 }) {
  ForEach(this.selectedProductList, (product: SelectedProduct) => {
    ListItem() {
      ElProductItem({ product })
    }
  })
}

给清空按钮添加事件

javascript 复制代码
.onClick(() => {
  clearAllProduct()
})

5.4 创建 ElProductCount 商品数量组件

javascript 复制代码
import { SelectedProduct } from '../models/Product'

@Component
export struct ElProductCount {
  product: SelectedProduct

  build() {
    Row({ space: 8 }) {
      Image($r('app.media.minus_circle'))
        .width(14)
        .aspectRatio(1)
        .fillColor("#02B6FD")

      Text('0').fontSize(14)

      Image($r('app.media.plus_circle'))
        .width(14)
        .aspectRatio(1)
        .fillColor("#02B6FD")
    }
  }
}

5.5 修改 ElProductItem 组件

在金额旁边添加数量组件

javascript 复制代码
ElProductCount({ product:this.product })

5.6 修改 ElProductCount 组件

  • 接收 product 数据
  • 接收 selectedProductList 数据
  • 获取该商品的数量
  • 给图标绑定添加商品,删除商品的事件
javascript 复制代码
import { SelectedProduct } from '../models/Product'
import { addProduct, removeProduct } from '../utils/productUtil'

@Component
export struct ElProductCount {
  @Consume selectedProductList: SelectedProduct[]
  product: SelectedProduct

  getCount() {
    const selectedProduct = this.selectedProductList.find(item => item.id === this.product.id)
    return selectedProduct?.count || 0
  }

  build() {
    Row({ space: 8 }) {
      Image($r('app.media.minus_circle'))
        .width(14)
        .aspectRatio(1)
        .fillColor("#02B6FD")
        .onClick(() => {
          removeProduct(this.product.id)
        })

      Text(`${this.getCount()}`).fontSize(14)

      Image($r('app.media.plus_circle'))
        .width(14)
        .aspectRatio(1)
        .fillColor("#02B6FD")
        .onClick(() => {
          addProduct(this.product)
        })
    }
  }
}

5.7 修改 ElSubtotal 组件

  • 接收已选中商品 selectedProductList
  • 添加商品总数据方法
  • 添加商品总金额方法
javascript 复制代码
@Component
export struct ElSubtotal {
  @Link showShoppingCart: boolean
  @Consume selectedProductList: SelectedProduct[]

  getTotalCount() {
    return this.selectedProductList.reduce((count, item) => {
      return count + item.count
    }, 0)
  }

  getTotalPrice() {
    return this.selectedProductList.reduce((price, item) => {
      return price + (item.count * item.price * 100)
    }, 0) / 100
  }
}

6. 文件

elshop.json文件

https://download.csdn.net/download/d312697510/89141677

icon图标

https://download.csdn.net/download/d312697510/89141683

git仓库地址

https://github.com/webdq/ElShop

相关推荐
zhanshuo3 小时前
在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
harmonyos
zhanshuo3 小时前
在鸿蒙中实现深色/浅色模式切换:从原理到可运行 Demo
harmonyos
烛阴4 小时前
前端必会:如何创建一个可随时取消的定时器
前端·javascript·typescript
whysqwhw8 小时前
鸿蒙分布式投屏
harmonyos
whysqwhw9 小时前
鸿蒙AVSession Kit
harmonyos
whysqwhw11 小时前
鸿蒙各种生命周期
harmonyos
whysqwhw12 小时前
鸿蒙音频编码
harmonyos
whysqwhw12 小时前
鸿蒙音频解码
harmonyos
whysqwhw12 小时前
鸿蒙视频解码
harmonyos
whysqwhw12 小时前
鸿蒙视频编码
harmonyos