#我的鸿蒙开发手记#鸿蒙门店UI与功能实现

云也蹦迪
发布于 2025-5-6 22:22
浏览
0收藏

#我的鸿蒙开发手记#鸿蒙门店UI与功能实现
一、说明与准备
1.整体说明
本模块是在华为手机上,基于 HarmonyOS 5.0.0 及以上版本、DevEco Studio 5.0.0 及以上版本等环境进行鸿蒙门店 UI 与功能实现的开发过程。
先对应用的整体构建布局设计进行了说明,包括模块设计中的门店模块(Stores.ets),以及门店 UI 布局开发中在 products 产品层下的 pages>MainEntry.ets 关键代码实现,展示了如何引入各功能模块组件并构建主布局容器、实现底部导航标签的切换等。
介绍了门店功能模块与实现流程,门店页面由城市选择器、门店列表、门店详情页等核心组件构成。重点对 Stores.ets 文件的创建与实现代码进行了讲解,包括自定义 Tab 页签构建器、城市筛选与门店列表动态加载的实现等。其中,StoreList.ets 实现了根据城市筛选门店并展示的功能,StoreCard.ets 则构建了门店卡片组件,实现了展示门店信息、拨打电话咨询、预约参观以及导航等功能。
还有权限配置这一重要环节,需在 module.json5 中声明相关权限以确保应用正常运行。
2.基础准备
(1)设备类型:华为手机
(2)HarmonyOS版本:HarmonyOS 5.0.0 Release及以上
(3)DevEco Studio版本:DevEco Studio 5.0.0 Release及以上
(4)HarmonyOS SDK版本:HarmonyOS 5.0.0 Release SDK及以上
二、门店整体构建布局设计
1.模块设计
门店模块:Stores.ets
2.门店UI布局开发
(1)products产品层下的pages>MainEntry.ets关键代码实现:

// 引入各功能模块组件
import { Home } from '@ohos_agcit/postpartum_care_center_home';      // 首页模块
import { Mine } from '@ohos_agcit/postpartum_care_center_mine';      // 我的模块
import { Stores } from '@ohos_agcit/postpartum_care_center_stores';  // 门店模块
import { Activities } from '@ohos_agcit/postpartum_care_center_activities'; // 活动模块
import { TAB_LIST } from '../contants/Constants';                    // 底部导航标签配置数据
import { MainEntryVM } from '@ohos_agcit/postpartum_care_center_uicomponents'; // 主入口视图模型
import { TabListItem } from '../model/TabListItem';                  // 标签项数据模型
import { Logger } from '@ohos_agcit/postpartum_care_center_utils';   // 日志工具类

const TAG: string = '[MainEntry]';  // 日志标签

@Entry                                // 标识为入口组件
@ComponentV2                          // 声明为V2版本组件
struct MainEntry {
  vm: MainEntryVM = MainEntryVM.instance;  // 绑定视图模型单例

  build() {
    Navigation(this.vm.navStack) {    // 创建导航容器,绑定导航栈
      Column() {                      // 主布局容器
        Tabs({ 
          barPosition: BarPosition.End,  // 标签栏位置:底部(End表示底端)
          index: this.vm.curIndex     // 当前选中标签索引
        }) {
          // ---------- 首页标签 ----------
          TabContent() {
            Home();                   // 加载首页组件
          }
          .clip(false)                // 禁用内容裁剪
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) // 适配顶部安全区域
          .tabBar(this.tabBarBuilder(TAB_LIST[0], 0)) // 绑定标签栏构建器

          // ---------- 门店标签 ----------
          TabContent() {
            Stores();                  // 加载门店组件
          }
          .clip(false)
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
          .tabBar(this.tabBarBuilder(TAB_LIST[1], 1))

          // ---------- 活动标签 ----------
          TabContent() {
            //Activities();              // 加载活动组件
          }
          .clip(false)
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
          .tabBar(this.tabBarBuilder(TAB_LIST[2], 2))

          // ---------- 我的标签 ----------
          TabContent() {
            //Mine();                    // 加载个人中心组件
          }
          .clip(false)
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
          .tabBar(this.tabBarBuilder(TAB_LIST[3], 3))
        }
        // Tabs全局配置
        .clip(false)
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
        .scrollable(false)             // 禁用标签栏滚动
        .height('100%')               
        .animationDuration(0)         // 切换动画时长0毫秒(无动画)
        .barMode(BarMode.Fixed)       // 标签栏模式:固定宽度
        .barHeight(56)                
        .onChange((index: number) => { // 标签切换事件监听
          this.vm.curIndex = index;   // 更新当前选中索引
        });
      }
      .clip(false)
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
    }
    // Navigation全局配置
    .height('100%')                   // 高度充满屏幕
    .clip(false)
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) // 适配顶部和底部安全区域
    .hideTitleBar(true)               // 隐藏标题栏
    .hideToolBar(true)                // 隐藏工具栏
    .hideBackButton(true)             // 隐藏返回按钮
    .mode(NavigationMode.Stack)       // 导航模式:堆栈模式
  }

  // 自定义标签栏构建器
  @Builder
  tabBarBuilder(item: TabListItem, index: number) {
    Column() {                        
      Image(this.vm.curIndex === index ? item.iconChecked : item.icon) // 动态切换选中/未选图标
        .width(24)                    
        .height(24);                  
      Text(item.label)                
        .fontColor(this.vm.curIndex === index ? 
          'rgba(0,0,0,0.90)' :        
          'rgba(0,0,0,0.60)')        
        .fontFamily('HarmonyHeiTi')   
        .fontWeight(this.vm.curIndex === index ? 
          FontWeight.Medium :         
          FontWeight.Regular)         
        .fontSize($r('app.string.font_size_10')) 
        .margin({ top: $r('app.string.margin_xxs') }); 
    }.width('100%');                  
  }
}

三、门店功能模块与实现流程
1.门店UI结构设计
门店页面由以下核心组件构成:
·城市选择器:支持按城市筛选门店。
·门店列表:展示门店名称、距离、特色服务标签及缩略图。
·门店详情页:包含套餐价目、环境图片、预约按钮、导航入口等。
2.功能实现步骤
(1)Stores.ets文件创建与实现代码:
①在之前创建好scenes目录下创建Stores模块(HSP动态共享)
#我的鸿蒙开发手记#鸿蒙门店UI与功能实现-鸿蒙开发者社区
②Stores.ets关键代码实现:

// 导入所需组件和数据模型
import { CITY_LIST, CityItem, MainEntryVM, TitleTop } from '@ohos_agcit/postpartum_care_center_uicomponents';
import { StoreList } from '../view/StoreList';
@Entry  
@Preview  
@ComponentV2  
export struct Stores {
  // 使用单例视图模型管理数据状态
  vm: MainEntryVM = MainEntryVM.instance;
  
  // 当前选中城市索引(@Local表示组件内部状态)
  @Local currentIndex: number = 0;
  
  // Tabs控制器实例(用于程序化控制页签切换)
  private tabsController: TabsController = new TabsController();

  /**
   * 自定义Tab页签构建器
   * @param title 城市名称 
   * @param targetIndex 对应城市ID
   */
  @Builder
  tabBarBuilder(title: string, targetIndex: number) {
    Column() {
      // 城市名称文本
      Text(title)
        .fontFamily('HarmonyHeiTi')  // 鸿蒙系统字体
        .fontWeight(this.currentIndex === targetIndex ? FontWeight.Bold : FontWeight.Regular)  // 选中态加粗
        .fontSize($r('app.string.font_size_14'))  
        .fontColor(this.currentIndex === targetIndex ? 'rgba(0,0,0,0.90)' : 'rgba(0,0,0,0.60)')  
        .margin({ bottom: $r('app.string.margin_9') });  
      
      // 选中态下划线指示器
      Divider()
        .width(28)  // 下划线宽度
        .strokeWidth(2)  // 线宽
        .color(this.currentIndex === targetIndex ? '#000000' : 'transparent')  // 选中时显示黑色
        .opacity(this.currentIndex === targetIndex ? 1 : 0);  // 非选中态完全透明
    }
    // 动态边距:首个标签左侧增加间距
    .margin(targetIndex === 0 ? 
      { left: $r('app.string.margin_ms'), right: $r('app.string.margin_s') } : 
      { left: $r('app.string.margin_s'), right: $r('app.string.margin_s') }
    )
    .padding({ 
      top: $r('app.string.padding_14'),  
      bottom: $r('app.string.padding_4')  
    })
    .height(48)  // 固定高度
    .onClick(() => {  // 点击事件
      this.currentIndex = targetIndex;
      this.tabsController.changeIndex(targetIndex);  // 联动切换Tabs内容
    });
  }

  build() {
    // 垂直布局容器
    Column() {
      // 顶部导航栏组件
      TitleTop({ title: $r('app.string.title_store') })  // "门店"(国际化资源)
        .margin({ bottom: $r('app.string.margin_xs') });  // 底部小间距
      
      // 横向Tab页签容器
      Tabs({ 
        barPosition: BarPosition.Start,  // 标签栏位于顶部
        controller: this.tabsController  // 绑定控制器
      }) {
        // 遍历城市数据生成Tab页
        ForEach(CITY_LIST, (item: CityItem) => {
          TabContent() {
            // 门店列表组件(传入当前城市名称)
            StoreList({ cityName: item.name });  // 复用门店列表组件
          }
          // 绑定自定义Tab栏样式
          .tabBar(this.tabBarBuilder(item.name, item.id));
        }, (item: CityItem) => JSON.stringify(item))  // 唯一键生成
      }
      // Tabs样式配置
      .vertical(false)  // 横向布局
      .barWidth('100%')  // 标签栏满宽
      .barMode(BarMode.Scrollable)  // 可滚动模式(城市较多时)
      .backgroundColor('#F1F3F5')  // 背景色与页面统一
      .scrollable(false)  // 禁用内容区域滚动
      .layoutWeight(1);  // 权重分配(撑满剩余空间)
    }
    // 页面级样式
    .backgroundColor('#F1F3F5')  // 页面背景色
    .clip(false)  // 允许子组件溢出
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]);  // 适配系统安全区域
  }
}

(2)步骤一:城市筛选与门店列表动态加载
描述:用户可通过下拉选择城市或自动获取定位加载附近门店。
StoreList.ets代码实现:

// 导入必要的组件和模块
import { MainEntryVM, StoreCard, StoreModel } from '@ohos_agcit/postpartum_care_center_uicomponents';
@Preview  
@ComponentV2  
export struct StoreList {
  // 组件参数:接收父组件传入的城市信息
  @Param cityName: string = '';  // 当前城市名称(如"北京")
  @Param cityId: string = '';    // 城市ID(预留字段)
  
  // 使用单例视图模型管理数据状态
  vm: MainEntryVM = MainEntryVM.instance;
  build() {
    // 垂直布局容器(占满父容器)
    Column() {
      // 滚动容器(支持上下滑动)
      Scroll() {
        // 列表容器(设置项间距12vp)
        List({ space: 12 }) {
          // 条件分支:处理"附近"特殊逻辑
          if (this.cityName === '附近') {
            // 遍历全部门店数据(无过滤)
            ForEach(this.vm.storeList.storeList, (item: StoreModel) => {
              ListItem() {
                // 单店卡片组件(传入门店数据)
                StoreCard({ store: item });  // 包含距离/套餐等信息
              };
            }, (item: StoreModel) => JSON.stringify(item));  // 唯一键生成
          } else {
            // 常规城市列表(按城市名称过滤)
            ForEach(this.vm.storeList.storeList, (item: StoreModel) => {
              // 城市匹配条件判断
              if (item.city === this.cityName) {
                ListItem() {
                  StoreCard({ store: item });
                };
              }
            }, (item: StoreModel) => JSON.stringify(item));
          }
        }
        .padding({
          left: $r('app.string.padding_16'),
          right: $r('app.string.padding_16'),
          top: $r('app.string.padding_8'),
          bottom: $r('app.string.padding_8'),
        });
      }
      .width('100%')
      .height('auto');
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start);
  }
}

(3)步骤二:门店详情页功能集成
描述:点击门店卡片跳转至详情页,展示套餐信息并支持预约、导航等操作。
StoreCard.ets代码实现:

import { call } from '@kit.TelephonyKit';          // 导入电话服务模块
import { BusinessError } from '@kit.BasicServicesKit';  // 导入基础服务错误类型
import { MainEntryVM } from '../viewmodel/MainEntryVM';  // 导入主入口视图模型
import { StoreModel } from '../model/StoreModel';        // 导入门店数据模型
import { ComponentContent } from '@kit.ArkUI';           // 导入ArkUI组件内容模块
import { PromptActionClass } from '@ohos_agcit/postpartum_care_center_utils';  // 导入自定义弹窗工具类
import { callPhone } from './StoreDetail';               // 导入拨打电话功能模块

const TAG: string = '[StoreCard]';  // 日志标签

// 构建电话弹窗组件的装饰器
@Builder
export function callDialog(callPhone: Function) {
  Column() {
    Row() {
      Text('025-0000 0000')  // 显示电话号码(需替换为动态数据)
        .fontSize(16)
        .margin({ top:24 })
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)  // 水平居中
    
    Row() {
      // 取消按钮
      Text('取消')
        .width('50%')
        .height(64)
        .fontSize(16)
        .fontColor('rgba(0,0,0,0.60)')
        .textAlign(TextAlign.Center)
        .onClick(() => {
          PromptActionClass.closeDialog();  // 关闭弹窗
        })

      // 呼叫按钮
      Text('呼叫')
        .onClick(() => {
          callPhone();                      // 执行拨号操作
          PromptActionClass.closeDialog();  // 关闭弹窗
        })
        .width('50%')
        .height(64)
        .fontColor('rgba(0,0,0,0.90)')
        .fontSize(16)
        .textAlign(TextAlign.Center)
    }
    .width('100%')
  }
  // 弹窗样式设置
  .width(328)
  .height(116)
  .borderRadius(32)
  .backgroundColor(Color.White)
}

// 门店卡片组件(预览模式)
@Preview
@ComponentV2
export struct StoreCard {
  vm: MainEntryVM = MainEntryVM.instance;  // 视图模型实例
  private ctx: UIContext = this.getUIContext();  // 获取UI上下文
  private contentNode: ComponentContent<object> = new ComponentContent(
    this.ctx,
    wrapBuilder(callDialog),  // 包装弹窗构建器
    callPhone
  )

  // 门店数据参数(默认值用于预览)
  @Param store: StoreModel = new StoreModel(
    1, 
    '南京涵江楼', 
    '软件大道101号', 
    '11100001111', 
    $r('app.media.store_pic1'), 
    '南京', 
    31.98, 
    118.76
  );

  // 组件构建函数
  build() {
    Column() {
      Stack() {
        // 门店图片展示
        Image(this.store.image)
          .width('100%')
          .borderRadius({ topLeft: $r('app.string.border_radius_16'), topRight: $r('app.string.border_radius_16') })
          .height(180)
          .objectFit(ImageFit.Cover);  // 图片填充模式

        // 距离显示(点击可重新请求定位权限)
        Text(this.store.distance !== null ? `距离${this.store.distance}km` : this.vm.locationServiceMessage)
          .fontSize($r('app.string.font_size_10'))
          .fontColor('#FFFFFF')
          .backgroundColor('rgba(0,0,0,0.40)')
          .position({ top: 8, right: 8 })
          .height(16)
          .onClick(() => {
            this.vm.requestLocationPermission();  // 点击触发定位权限请求
          });
      }
      .width('100%')
      .height(180)
      .margin({ bottom: $r('app.string.margin_ms') });

      // 门店名称
      Text(this.store.name)
        .fontColor('rgba(0,0,0,0.90)')
        .fontSize($r('app.string.font_size_16'))
        .margin({ bottom: $r('app.string.margin_xxs') });

      // 地址与导航图标行
      Row() {
        Text(this.store.address)  // 地址文本
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis });  // 超出部分显示省略号
        
        Image($r('app.media.ic_navigation'))  // 导航图标
          .width(16)
          .height(16)
          .margin({ left: $r('app.string.margin_xxs') });
      }
      .onClick(() => {
        this.vm.queryLocation(this.store);  // 点击触发地址查询
      });

      // 操作按钮行
      Row({ space: 24 }) {
        // 电话咨询按钮
        Button({ type: ButtonType.Normal }) {
          Text($r('app.string.visit_Consulting'))  // 文本资源引用
            .fontSize($r('app.string.font_size_12'));
        }
        .backgroundColor('#FFFFFF')
        .borderColor('rgba(0,0,0,0.40)')
        .onClick(() => {
          // 打开电话弹窗
          PromptActionClass.openDialog(this.getUIContext(), this.contentNode);
        });

        // 预约参观按钮
        Button({ type: ButtonType.Normal }) {
          Text($r('app.string.visit_booking'))
            .fontColor('#FFFFFF');
        }
        .backgroundColor('#333333')
        .onClick(() => {
          // 跳转到预约页面
          this.vm.navStack.pushPathByName('Booking', this.store.name);
        });
      }
    }
    // 卡片整体样式
    .shadow({ radius: 8, color: 'rgba(0,0,0,0.08)' })  // 阴影效果
    .height(312)
    .borderRadius($r('app.string.border_radius_16'))
    .backgroundColor('#FFFFFF')
    .onClick(() => {
      // 点击卡片跳转到详情页
      const params: Record<string, Object> = { 'store': this.store, 'contentNode': this.contentNode };
      this.vm.navStack.pushPathByName('StoreDetail', params);
    });
  }
}

四、最终成果展示
#我的鸿蒙开发手记#鸿蒙门店UI与功能实现-鸿蒙开发者社区
五、注意事项
权限配置:需在module.json5中声明以下权限

"reqPermissions": [  
  { "name": "ohos.permission.LOCATION" },  
  { "name": "ohos.permission.WRITE_CALENDAR" }  ]  

收藏
回复
举报
回复
    相关推荐