HarmonyOS开发 - 餐饮APP首页开发实例

        一个餐饮APP开发,主要包括以下几个核心步骤和功能:

  1. 需求和功能规划:首先需要确定APP的基本功能,如商品展示、在线点餐、订单管理、支付结算、扫码核销等。同时需要实现多门店特色功能,如门店切换与定位、库存管理和销售数据统计等。此外,还有营销功能、如优惠券发放、会员制度、积分兑换等,以吸引顾客和提高用户的粘性。
  2. 选择开发方式:如企业有自己的技术团队,可以选择自主研发,以实现高度定制化。如没有,则可以选择第三方开发平台或外包给专业的软件公司,以降低成本和提高效率。
  3. 申请账号和认证:注册相关的账号并进行企业主体认证,以确保可以使用APP的更多功能,如支付接口等。
  4. 设计与开发:设计简洁、美观、易用的界面,确保各个功能模块的正常运行和数据的准确传输。特别是多门店管理系统的开发,需要建立一个集中的管理后台,用于管理各个门店的信息、菜品、库存、订单等,以使总部可以统一管理和监控。
  5. 测试与优化:在开发完成后,进行功能测试和优化,确保APP的稳定性和用户体验。测试内容包括订单处理、支付功能、用户界面等方面。
  6. 多门店管理:实现多门店的统一管理和调试,实时监控各门店的运营情况,确保高效顺畅。
  7. 营销工具:支持优惠券、满减活动、会员积会等多种促销方式,提升客户黏性和销售额。
  8. 支付功能:支持多种支付方式,确保支付案例,提升顾客满意度。

        首先我们先完成APP的首页开发,其中包含商品展示轮播图、应用分类、商品列表、底部导航等功能。

 一、环境搭建

1.1 DevEco Studio

        如果DevEco Studio应用已安装,则直接打开并创建项目即可。

9cdc05b97c0440aa8a4d08e438e7735f.png

        如系统中未安装DevEco Studio开发工具,可前云官方网站下载,地址:DevEco Studio-HarmonyOS Next Beta版-华为开发者联盟

        安装步骤地址:HarmonyOS开发之DevEco Studio安装_select the directory where ohpm has been installed-****博客

2.2 仿真机安装

        如果你没有华为系统手机,可以点击并打开Device Manager管理界面,选择“Local Emulator”本地仿真器的创建。

e41d7dc8f1164ec8860327a44f007d86.png

        在Device Manager管理界面的右下角,点击“New Emulator”新仿真器创建按钮,选择您需要创建的仿真设备即可,这里选择“Huawei_Phone”。

065cce1fd36347939a7c29c3e475c09d.png

        然后点击下一步,进入虚拟设备的配置界面,选择您需要的版本即可。这里选择最新版本(注意:在点击”下一步“按钮前,需要点击Name列中下载图标,将需要安装的对应版本先下载,否则会提示“Download the system image first”错误),如下图:

965be6db70154e92bdf9d168dba70127.png

        点击“下一步”后,会进入新界面,自定义设备名称后,点击“完成”即可。如下图:

e4d03eb0e2794cdea1b5be351edeaebe.png

        当仿真设备创建完成后,最初管理界面会多出新创建仿真设备信息,如下图:

1c64d06903eb4038abc1c177427753f2.png

        此时,点击右侧“播放”按钮,则可以启动仿真设备了。如下图:

7bc25e7ac63c4355814fe4e3cf7cd41c.png

二、创建APP应用

        创建项目应用,首先打开DevEco Studio开发工具,点击“File” -> “New” -> “Create Project”,打开

d1af06d436aa435cbf9640bcdb3362e6.png

        选择创建一个空应用,点击“下一步”。

c880086644be4ccb95d274955edff5ba.png

        修改项目名称后,点击“完成”即可。

7a17d5b0cc524abd81eef552ffa100a4.png

        此时项目则已创建成功,如下图:

a0dc868b0ae241a39f7141feb1921b1d.png

2.1 Previewer预览        

        点击右侧的“previewer”即可查看界面效果,如下图:

32c4d4a322b74fde918ac54db0eaf1dc.png

2.2 仿真设备预览

        当仿真设备启动后,在DevEco Studio中会显示创建的仿真设备的名称,此时点击右侧“Run”或“Debug”即可在仿真设备中预览界面效果了。

81b0d61f942a4bab93421db79438e9a1.png

        点击“Run”后,刚创建的项目界面,则在仿真设备中显示了,如下图:

5df7631def814ef88c09f6ac74789695.png

三、餐饮APP首页

        在介绍完HarmonyOS项目开发中,对DevEco Studio安装、仿真设备创建后,接下来将通过HarmonyOS完成如下图的首页的搭建工作。

dd81b0eea52b437e87e3c6ce215eea06.png

       上图原型已上传到资源中,需要的朋友可以前去下载,地址:https://download.****.net/download/jiciqiang/89906021

        图片资源,可下载原型资源包后,打开压缩包并解压,在image目录中查看。如下图:

cbd933fd8c404675bd6d6ec6e173d887.png

3.1 自定义组件 - 低代码开发

        在创建顶部导航前,我们先在ets目录中新建components目录,用于存放自定义组件。

e936441520ec4066ac5b5250c7a9f6b6.png

f4797fd5596c4439bcd97984e7cb6b85.png

        当components创建成功后,在其内部创建Header.ets文件,用于自定义顶部导航组件。

089e2b9ef8164387ac396ebf7b13d0b1.png

        选中components目录并右击,创建组件,如下图:

5c6cd5b158544388a92eeb1b3ff4f4db.png

        此操作会创建出一个低代码操作界面,可以帮助开发人员快速搭建界面布局及样式设置。如果您还是习惯于手动代码搭建,可以直接在components目录中创建ets文件,定义Header组件。低代码界面如下图:

3a3782b571004191b9cb2669bf0c61f2.png

        当我们点击“生成est文件”转换按钮后,会弹出要生成的代码弹框,如下图:

095525ada68246048521c71f51e783d4.png

        进行转换后再打开Header.ets文件,则代码已经生成。此时大家会发现,低代码文件自动删除了,这操作是不可逆的过程。

c76b3742516d407a80897c65ece19853.png

说明:使用低代码开发界面过程中,如果界面需要使用到其他暂时不支持可视化布局的控件,可以在低代码界面开发完成后,单独“转换”按钮,将低代码界面转换为ets文件。

注意:代码转换操作会删除visual文件及其父目录,且为不可逆过程,代码转换后不能通过html/css或ets文件反向生成visual文件。多设备开发的场景,可以单击界面画布query模式,可以为组件设置不同的样式和属性。当前media query模式仅针对不同设备类型和不同屏幕状态(横屏/竖屏)有效。

        DevEco低代码文档地址:文档中心

3.2 自定义组件 - 顶部导航       

        顶部导航组件开内容和样式开发完成后,可以打开pages目录下的index.ets文件,将Header组件引入进去后,再来查看下效果。

        代码如下:

import Header from '../components/Header'
@Entry
@Component
struct Index {
  @State message: string = 'Hello World'

  build() {
    Row() {
      Column() {
        Header()

        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%').alignItems(VerticalAlign.Top)
  }
}

        界面效果如下图:

349920aa963c44afa05403ec2b896bd6.png

        界面内容和样式虽然已完成了,但是当搜索内容改变时,需要将输入文本上报给父组件,从而做出相应操作及跳转动作。所以我们在Header.ets文件添加相应变量、方法和事件,将信息传递给父组件。

components/Header.ets代码如下:

@Preview
@Component
export default struct Header {
  // 搜索关键词
  @State keyword: string = ''
  // 关键词内容改变时回调函数
  private onSearchChange: (data: string) => void

  build() {
    Row() {
      Image($rawfile('position_u5.png'))
        .width("16vp")
        .height("25vp")
        .margin({ top: "0vp", bottom: "0vp", left: "0vp", right: "0vp" })
      Text("门店信息\n")
        .width("65vp")
        .height("40vp")
        .fontSize("12fp")
      TextInput({ text: this.keyword, placeholder: "请输入搜索关键词" })
        .width("170vp")
        .height("40vp")
        .placeholderColor("#262626")
        .placeholderFont({ size: 12 })
        .onChange((e) => this.keyword = e)
      Button("搜索")
        .width("70vp")
        .height("40vp")
        .onClick(() => {
          this.onSearchChange(this.keyword) // 点击查询事件,将信息传递给父组件
        })
    }    
    .width("100%")
    .padding({ top: "10vp", bottom: "10vp", left: "15vp", right: "15vp" })
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

pages/Index.ets代码如下:

import Header from '../components/Header'
@Entry
@Component
struct Index {
  @State keyword: string = ''

  build() {
    Row() {
      Column() {
        Header({onSearchChange: this.onSearchChange.bind(this)})
      }
      .width('100%')
    }
    .height('100%').alignItems(VerticalAlign.Top)
  }
  /**
   * 搜索内容改变事件
   * @param data
   */
  onSearchChange(data: string){
    console.log('search', data)
  }
}

        此时点击顶部导航栏中“搜索”按钮,则可以在父组件中输出输入框中的内容了。如下图:

3e3f9af26a6c4b469f70f4fcc24bbadf.png

0a725ef92ab04787bea0071e9d3d7e3a.png

        该篇目前只讲首页搭建部分内容,所以搜索功能后续操作暂时不再详说,后续有机会或者大家自行追加即可。

3.3 自定义组件 - 底部菜单

ff4f3d4985994a62a958188d2f74153d.png

        接下来我们来完善下底部菜单导航的开发工作,这里将使用到UI界面的Tabs组件。

3.3.1 Tabs组件

        Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。TabContent是内容页,TabBar是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。

35b2dd93cda541a9830b3a47b6782e52.png

说明:

  • TabContent组件不支持设置通用宽度属性,其宽度默认撑满Tabs父组件。
  • TabContent组件不支持设置通用高度属性,其高度由Tabs父组件高度与TabBar组件高度决定。

        Tabs使用花括号包裹TabContent,如下图,其中TabContent显示相应的内容页。

6c82dac3fd284d3787673daf9a127c81.png

        官方文档地址:文档中心

3.3.2 实现Tabs菜单

        了解了Tabs结构后,我们使用它完成底部导航菜单代码部分。visual低代码模式中也有Tabs组件,由于这里没有复杂的样式结构,直接在页面中定义即可。

        pages/index.ets代码如下:

import Header from '../components/Header'
@Entry
@Component
struct Index {
  @State keyword: string = ''

  build() {
    Row() {
      Column() {
        Tabs({
          barPosition: BarPosition.End
        }){
          TabContent(){
            Header({onSearchChange: this.onSearchChange.bind(this)})
          }.tabBar({icon: $rawfile('u48.png'), text: '首页'})
          TabContent(){
            Text('待开发中...')
          }.tabBar({icon: $rawfile('u43.png'), text: '订单'})
          TabContent(){
            Text('待开发中...')
          }.tabBar({icon: $rawfile('u53.png'), text: '我的'})
        }
        .barMode(BarMode.Fixed)
        .barHeight(60)
      }
      .width('100%')
    }
    .height('100%').alignItems(VerticalAlign.Top)
  }
  /**
   * 搜索内容改变事件
   * @param data
   */
  onSearchChange(data: string){
    console.log('search value:', data)
  }
}

        页面效果如下图:

ec38ddf4a4d74092b9897590ef5e53ba.png

3.4 自定义构建函数 - 轮播图

        Swiper组件提供滑动轮播显示的能力。Swiper本身是一个容器组件,当设置了多个子组件后,可以对这些子组件进行轮播显示。通常,在一些应用首页显示推荐的内容时,需要用到轮播显示的能力。

        在components/Index目录中创建轮播图组件,文件结构如下图:

d096d2ad3fc2403fb07b7700715f8aea.png

        代码如下:

@Preview
@Component
export default struct CarouselMap {
  private swiperController: SwiperController = new SwiperController()

  build() {
    Row() {
      Swiper(this.swiperController){
          Image($rawfile('u13.png')).borderRadius(10)
          Image($rawfile('u13.png')).borderRadius(10)
        }
        .width("100%")
        .height("200vp")
        .padding(10)
        .index(0)         // 默认索引
        .autoPlay(true)   // 自动播放
        .loop(true)       // 循环播放
        .itemSpace(10)    // 图片的间隙
        .onChange((index: number) => {
          console.log('testTag swiper index:', index)
        })
    }
  }
}

        当轮播图组件定义好后,将期引入到首页中,pages/index.ets代码如下:

import Header from '../components/Header'
import CarouseMap from '../components/Index/CarouselMap'
@Entry
@Component
struct Index {
  @State keyword: string = ''

  build() {
    Row() {
      Column() {
        Tabs({
          barPosition: BarPosition.End
        }){
          TabContent(){
            // 增加Column原因是因为TabContent中只有一个子组件,否则会报错
            Column(){
              Header({onSearchChange: this.onSearchChange.bind(this)})
              CarouseMap()
            }.width('100%').height('100%').alignItems(HorizontalAlign.Start)
          }.tabBar({icon: $rawfile('u48.png'), text: '首页'})
          TabContent(){
            Text('待开发中...')
          }.tabBar({icon: $rawfile('u43.png'), text: '订单'})
          TabContent(){
            Text('待开发中...')
          }.tabBar({icon: $rawfile('u53.png'), text: '我的'})
        }
        .barMode(BarMode.Fixed)
        .barHeight(60)
      }
      .width('100%')
    }
    .height('100%').alignItems(VerticalAlign.Top)
  }
  /**
   * 搜索内容改变事件
   * @param data
   */
  onSearchChange(data: string){
    console.log('search value:', data)
  }
}

        页面效果如下图:

6d80d84fdcdd4ba987bd266c22ba088f.png

3.5 自定义构建函数 - 分类列表

       接下来完成分类列表信息,在components/Index目录中创建CategoryList.ets组件。

0678c7939e33467e83c4cba005f36149.png

        创建后visual低代码编辑界面,如下图:

dcd9a5d0598c48e6819b62d2b5bf5a40.png

        当界面内容搭建完后,通过转换按钮,生成分类列表的组件代码。代码如下:

@Preview
@Component
export default struct MiddleMenu {

  build() {
    Row() {
      Column() {
        Image($rawfile('u27.png'))
          .width("64vp")
          .height("64vp")
        Text("门店")
          .height("32vp")
          .textAlign(TextAlign.Center)
          .fontSize("16fp")
          .fontWeight(FontWeight.Bold)
      }
      Column() {
        Image($rawfile('u25.png'))
          .width("64vp")
          .height("64vp")
        Text("水果")
          .height("32vp")
          .textAlign(TextAlign.Center)
          .fontSize("16fp")
          .fontWeight(FontWeight.Bold)
      }
      Column() {
        Image($rawfile('u27.png'))
          .width("64vp")
          .height("64vp")
        Text("美食")
          .height("32vp")
          .textAlign(TextAlign.Center)
          .fontSize("16fp")
          .fontWeight(FontWeight.Bold)
      }
      Column() {
        Image($rawfile('u29.png'))
          .width("64vp")
          .height("64vp")
        Text("包厢")
          .height("32vp")
          .textAlign(TextAlign.Center)
          .fontSize("16fp")
          .fontWeight(FontWeight.Bold)
      }
    }    
    .width("100%")
    .padding({ top: 10, bottom: 10, left: 20, right: 20 })
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

        再将分类列表组件引入到首页中,代码如下:

import Header from '../components/Header'
import CarouseMap from '../components/Index/CarouselMap'
import CategoryList from '../components/Index/CategoryList'
@Entry
@Component
struct Index {
  @State keyword: string = ''

  build() {
    Row() {
      Column() {
        Tabs({
          barPosition: BarPosition.End
        }){
          TabContent(){
            // 增加Column原因是因为TabContent中只有一个子组件,否则会报错
            Column(){
              Header({onSearchChange: this.onSearchChange.bind(this)})    // 顶部导航
              CarouseMap()        // 轮播图
              CategoryList()      // 分类列表
            }.width('100%').height('100%').alignItems(HorizontalAlign.Start)
          }.tabBar({icon: $rawfile('u48.png'), text: '首页'})
          TabContent(){
            Text('待开发中...')
          }.tabBar({icon: $rawfile('u43.png'), text: '订单'})
          TabContent(){
            Text('待开发中...')
          }.tabBar({icon: $rawfile('u53.png'), text: '我的'})
        }
        .barMode(BarMode.Fixed)
        .barHeight(60)
      }
      .width('100%')
    }
    .height('100%').alignItems(VerticalAlign.Top)
  }
  /**
   * 搜索内容改变事件
   * @param data
   */
  onSearchChange(data: string){
    console.log('search value:', data)
  }
}

        页面效果如下图:

48d0882d25ef4541aa5fb4dff6aa9a18.png

3.6 自定义构建函数 - 产品列表

        最后,再创建一个产品列表,在components/Index目录中创建ProductList组件。

27a7dd22606b4e0ea25d15a337229b58.png

3.6.1 visual低代码编辑

        这部分样式结构会稍微复杂点,所以在visual文件的低代码模式下,先画出基本轮廓和第一个产品信息,其他等转换为代码模式时,通过列表循环输出即可。如下图:

1998aedd55b24b208520bb978208e660.png

3.6.2 转换为代码

        转换后的代码,只是基本结构和部分产品样式,代码如下:

@Preview
@Component
export default struct ProductList {

  build() {
    Row({ space: "0vp" }) {
      Column() {
        Text("精品特卖")
          .width("100%")
          .height("50vp")
          .fontSize("16fp")
          .fontWeight(FontWeight.Bold)
        Row() {
          Row() {
            Column() {
              Column() {
                Image($rawfile('u62.png'))
                  .width("150vp")
                  .height("65vp")
                Text("啫啫鱼头")
                  .width("150vp")
                  .height("26vp")
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .fontSize("10fp")
                Row() {
                  Text("¥52")
                    .width("60vp")
                    .height("30vp")
                    .fontColor("#ff0000")
                    .fontSize("12fp")
                    .fontWeight(FontWeight.Bold)
                  Text("原价:55元")
                    .width("90vp")
                    .height("30vp")
                    .fontColor("#575757")
                    .decoration({ type: TextDecorationType.LineThrough, color: "#313030" })
                }                
                .width("150vp")
              }
            }
            Column()
          }
        }
        Row() {
          Column()
          Column()
        }
      }      
      .width("100%")
    }    
    .width("100%")
    .padding({ top: "10vp", bottom: "10vp", left: "15vp", right: "15vp" })
  }
}

        此时将其引入到首页中效果是这样的,如下图:

45e8ba724da84dd2b6e8f443dd572159.png

        产品信息是通过接口获取的列表数据,然后通过GridRow组件和GridCol组件组合使用,将产品渲染成两列。这里暂时未调用接口,所以先将数据定义在组件中,通过ForEach遍历数组。

        components/Index/ProductList.ets代码如下:


type ProductType = {
  title: string
  thumb: Resource
  price: number
  originPrice: number
}

@Preview
@Component
export default struct ProductList {
  @State productList: Array<ProductType> = [
    { title: '啫啫鱼头', thumb: $rawfile('u62.png'), price: 52, originPrice: 55 },
    { title: '黑椒汁啫牛肉', thumb: $rawfile('u76.png'), price: 55, originPrice: 60 },
    { title: '啫啫田鸡', thumb: $rawfile('u90.png'), price: 52, originPrice: 55 },
    { title: '咸蛋黄啫苦瓜', thumb: $rawfile('u104.png'), price: 26, originPrice: 30 }
  ]
  // 定义内联构建函数
  @Builder ImageItems(item: ProductType){
    Row(){
      Column(){
        Image(item.thumb)
          .width("100%")
          .height("100vp")
          .borderRadius(8)
        Text(item.title)
          .width("100%")
          .height("36vp")
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .fontSize("18fp")
        Row() {
          Text("¥" + item.price)
            .width("40%")
            .fontColor("#ff0000")
            .fontSize("18fp")
            .fontWeight(FontWeight.Bold)
          Text("原价:" + item.originPrice + "元")
            .width("60%")
            .fontColor("#575757")
            .fontSize("12fp")
            .decoration({ type: TextDecorationType.LineThrough, color: "#313030" })
        }
        .width("100%").alignItems(VerticalAlign.Center)
      }
    }.width('100%').padding(10)
  }

  build() {
    Row({ space: "0vp" }) {
      Column() {
        Text("精品特卖")
          .width("100%")
          .height("50vp")
          .fontSize("16fp")
          .fontWeight(FontWeight.Bold)
          .padding(10)
        // 每行分两列展示
        GridRow({columns: 2}){
          ForEach(this.productList, (item: ProductType, index) => {
            GridCol(){
              this.ImageItems(item)
            }
          })
        }
      }.padding(5)
    }
    .width("100%")
  }
}

        此时首页就已完成了,效果如下图:

3b5ae9c2c6f44e48abf7e4111586bfbf.png

3.6.3 滚动条

        不过此时,大家可能会发现内容超出来屏幕可见区域,无法上拉显示。这个问题比较简单,可以使用List和ListItem组件解决。

        pages/index.ets最终代码如下:

import Header from '../components/Header'
import CarouseMap from '../components/Index/CarouselMap'
import CategoryList from '../components/Index/CategoryList'
import ProductList from '../components/Index/ProductList'
@Entry
@Component
struct Index {
  @State keyword: string = ''

  build() {
    Row() {
      Column() {
        Tabs({
          barPosition: BarPosition.End
        }){
          TabContent(){
            // 增加Column原因是因为TabContent中只有一个子组件,否则会报错
            Column(){
              Header({onSearchChange: this.onSearchChange.bind(this)})    // 顶部导航
              List(){
                ListItem(){
                  Column(){
                    CarouseMap()        // 轮播图
                    CategoryList()      // 分类列表
                    ProductList()       // 产品列表
                  }
                }
              }.layoutWeight(1)
            }.width('100%').height('100%').alignItems(HorizontalAlign.Start)
          }.tabBar({icon: $rawfile('u48.png'), text: '首页'})
          TabContent(){
            Text('待开发中...')
          }.tabBar({icon: $rawfile('u43.png'), text: '订单'})
          TabContent(){
            Text('待开发中...')
          }.tabBar({icon: $rawfile('u53.png'), text: '我的'})
        }
        .barMode(BarMode.Fixed)
        .barHeight(60)
      }
      .width('100%')
    }
    .height('100%').alignItems(VerticalAlign.Top)
  }
  /**
   * 搜索内容改变事件
   * @param data
   */
  onSearchChange(data: string){
    console.log('search value:', data)
  }
}

        此时页面超出部分则可以上拉查看了,如下图:

1e0ad74db35e4301a3077ed165b3cf53.png

        这就是首页全部静态代码了,后期须根据接口数据动态渲染及增加交互功能,这期就不讲解了。希望对大家有所帮助~

上一篇:冯诺依曼架构及CPU相关概念


下一篇:ueLsitview 实现自动滚动