Harmony 个人中心(页面交互、跳转、导航、容器组件)(下)

简介: Harmony 个人中心(页面交互、跳转、导航、容器组件)(下)

Harmony 个人中心(页面交互、跳转、导航、容器组件)(上)https://developer.aliyun.com/article/1407912


三、导航栏


  登录后我们进入Index页面,也就是主页面,我们先看看主页面的内容

b48a456b10cb4a7aa74432578566045d.png

738066cc92e745e59bde2fa0a4395dbc.png

通过这两张图,我们可以看到,主页面有两部分,选项卡和选项卡内容,通过底部选项卡点击进行切换,那么在写这个页面的时候应该怎么入手呢?首先我们应该先写选项卡,也就是底部导航这一部分内容。


下面我们修改一下Index.ets中的代码,如下所示:

@Entry
@Component
struct Index {
  @State currentIndex: number = 0
  private tabsController: TabsController = new TabsController()
  @Builder TabBuilder(title: string, index: number, selectedImg: Resource, normalImg: Resource) {
    Column() {
      Image(this.currentIndex === index ? selectedImg : normalImg)
        .width(24)
        .height(24)
      Text(title)
        .margin({ top: 4 })
        .fontSize(10)
        .fontColor(this.currentIndex === index ? $r('app.color.mainPage_selected') : $r('app.color.mainPage_normal'))
    }
    .justifyContent(FlexAlign.Center)
    .height(26)
    .width('100%')
    .onClick(() => {
      this.currentIndex = index
      this.tabsController.changeIndex(this.currentIndex)
    })
  }
  build() {
    Tabs({
      barPosition: BarPosition.End,
      controller: this.tabsController
    }) {
      TabContent() {
        // 首页内容
      }
      .padding({ left: 12, right: 12 })
      .backgroundColor($r('app.color.mainPage_backgroundColor'))
      .tabBar(this.TabBuilder('首页', 0, $r('app.media.home_selected'), $r('app.media.home_normal')))
      TabContent() {
        // 我的内容
      }
      .padding({ left: 12, right: 12 })
      .backgroundColor($r('app.color.mainPage_backgroundColor'))
      .tabBar(this.TabBuilder('我的', 1, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
    }
    .width('100%')
    .backgroundColor(Color.White)
    .barHeight(56)
    .barMode(BarMode.Fixed)
    .onChange((index: number) => {
      this.currentIndex = index
    })
  }
}


  下面我们来分析一下这段代码,首先我们定义了currentIndex变量,用于记录当前选项卡的下标,然后定义了一个tabsController,用于进行选项卡的控制,接下来使用@Builder装饰器来构建Tab的内容,使用纵向布局将图标和文字居中摆放,根据currentIndex和当前Index的判断来进行Tab的选中、未选中状态。currentIndex默认为0,则是默认选中第一个Tab,也就是首页Tab,在Tab的点击事件中,我们更新currentIndex的值,然后再使用this.tabsController.changeIndex(this.currentIndex)进行切换Tab选项。


  然后来看build()函数中的代码,这里我们使用了Tabs()组件,通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。我们看里面传的参数,这里重点是第一个参数,这个的barPosition不是下标的意思,而是设置Tabs的页签位置。默认值:BarPosition.Start,这里的默认值实际上还要结合Tabs组件的vertical属性来结合使用。


  vertical设置为false是为横向Tabs,设置为true时为纵向Tabs。默认值:false,我们没有在代码中设置这个属性,所以默认就是纵向的,那么我们再结合这个BarPosition的值来看:

  • Start,vertical属性方法设置为true时,页签位于容器左侧;vertical属性方法设置为false时,页签位于容器顶部。
  • End,vertical属性方法设置为true时,页签位于容器右侧;vertical属性方法设置为false时,页签位于容器底部。


  那么现在就是Tabs就是在屏幕底部,Tabs可以在屏幕上下左右进行摆放。

  在Tabs()中放置了两个TabContent(),TabContent,仅在Tabs中使用,对应一个切换页签的内容视图,这个内容视图我们后面来写,这个组件有一个tabBar()属性,用于装载Tab内容,这里就用到我们之前所构建的TabBuilder()函数。


  最后我们再了解一下Tabs()组件的其它两个属性:

  1. BarMode有两个属性,1. Scrollable:每一个TabBar均使用实际布局宽度,超过总长度(横向Tabs的barWidth,纵向Tabs的barHeight)后可滑动。2. Fixed:所有TabBar平均分配barWidth宽度(纵向时平均分配barHeight高度)。
  2. onChange,Tab页签切换后触发的事件。index:当前显示的index索引,索引从0开始计算。触发该事件的条件:1、TabContent支持滑动时,组件触发滑动时触发。2、通过控制器API接口调用。3、通过状态变量构造的属性值进行修改。4、通过页签处点击触发。


通过这些说明,相信你已经知道Tabs()的用法了,下面我们保存预览一下Index,默认是Home,点击Mine,如下图所示:

4c40fa45b4d04903851ef38108b70100.png

四、首页


在写这个首页的内容之前,我们先看一下整个页面的布局,如图

b48a456b10cb4a7aa74432578566045d.png

首页内容呈纵向摆放,同时需要考虑屏幕大小,因此我们可以加一个滑动控件,再看里面的内容,首先是一个标题,标题下面是轮播图,然后是两个网格列表。这样页面内容就介绍完了,那么我么应该怎么来写这个页面的内容呢?


① 轮播图

  首先我们完成标题和轮播图,在ets下创建一个viewmodel包,该包下创建一个IndexViewModel.ets文件,代码如下所示:

export class IndexViewModel {
  /**
   * 获取轮播图数据
   */
  getSwiperImages(): Array<Resource> {
    let swiperImages: Resource[] = [
      $r('app.media.fig1'),
      $r('app.media.fig2'),
      $r('app.media.fig3'),
      $r('app.media.fig4')
    ]
    return swiperImages
  }
}
export default new IndexViewModel()


通过这个getSwiperImages()来获取轮播图数据,下面我们可以构建主页面的组件了,在在ets下创建一个view包,包下新建一个Home.ets文件,里面代码如下所示:

import mainViewModel from '../viewmodel/IndexViewModel';
/**
 * 首页
 */
@Component
export default struct Home {
  private swiperController: SwiperController = new SwiperController();
  build() {
    Scroll() {
      Column({ space: 12 }) {
        //首页
        Column() {
          Text('首页')
            .fontWeight(FontWeight.Medium)
            .fontSize(24)
            .margin({ top: 12 })
            .padding({ left: 12 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        //轮播图
        Swiper(this.swiperController) {
          ForEach(mainViewModel.getSwiperImages(), (img: Resource) => {
            Image(img).borderRadius(16)
          }, (img: Resource) => JSON.stringify(img.id))
        }
        .margin({ top: 24 })
        .autoPlay(true)
      }
    }
    .height('100%')
  }
}


这里的代码就是一个按照我们上面所说的思路来设计的,滚动条里面有标题和轮播图,并设置轮播图自动轮播,在滚动组件中内容未填满页面高度的情况下,内容就会居中显示,我们将Home放在Index中,如下图所示:

8cffdcbc9ddc4a768a9cbe30f2533372.png

然后我们预览Index,看看预览效果图:

97f92cd943344167bcefc11662585a5b.png

② 网格列表

下面我们再来写网格列表,首先要做的就是制造一些数据,先创建一个数据Bean,在ets下创建一个bean包,该包下创建一个ItemData.ets文件,代码如下所示:

export default class ItemData {
  title: Resource|string;
  img: Resource;
  others?: Resource|string;
  constructor(title: Resource|string, img: Resource, others?: Resource|string) {
    this.title = title;
    this.img = img;
    this.others = others;
  }
}


这个Bean中只有三个数据,标题、图片,其他。下面我们在IndexViewModel中制造一些假数据,写两个函数,代码如下所示:

  /**
   * 获取第一个网格数据
   */
  getFirstGridData(): Array<ItemData> {
    let firstGridData: ItemData[] = [
      new ItemData('我的最爱', $r('app.media.love')),
      new ItemData('历史记录', $r('app.media.record')),
      new ItemData('消息', $r('app.media.message')),
      new ItemData('购物车', $r('app.media.shopping')),
      new ItemData('我的目标', $r('app.media.target')),
      new ItemData('圈子', $r('app.media.circle')),
      new ItemData('收藏', $r('app.media.favorite')),
      new ItemData('回收站', $r('app.media.recycle'))
    ]
    return firstGridData
  }
  /**
   * 获取第二个网格数据
   */
  getSecondGridData(): Array<ItemData> {
    let secondGridData: ItemData[] = [
      new ItemData('排行榜', $r('app.media.top'), '当前热品尽在掌握'),
      new ItemData('新品首发', $r('app.media.new'), '最新潮牌,马上发布'),
      new ItemData('大牌闪购', $r('app.media.brand'), '更多大牌敬请期待'),
      new ItemData('发现好物', $r('app.media.found'), '更多内容等您探索')
    ]
    return secondGridData
  }


这里我们需要导入ItemData,还记得是怎么导入的吗?因为创建others?: Resource|string;的时候,使用了一个?,表示可以为空,下面我们在Home中增加这两个网格的UI展示,代码如下所示:

import mainViewModel from '../viewmodel/IndexViewModel';
import ItemData from '../bean/ItemData';
/**
 * 首页
 */
@Component
export default struct Home {
  private swiperController: SwiperController = new SwiperController();
  build() {
    Scroll() {
      Column({ space: 12 }) {
        //首页
        ...
        //轮播图
        ...
        //第一个网格布局
        Grid() {
          ForEach(mainViewModel.getFirstGridData(), (item: ItemData) => {
            GridItem() {
              Column() {
                Image(item.img)
                  .width(24)
                  .height(24)
                Text(item.title)
                  .fontSize(12)
                  .margin({ top: 4 })
              }
            }
          }, (item: ItemData) => JSON.stringify(item))
        }
        .columnsTemplate('1fr 1fr 1fr 1fr')
        .rowsTemplate('1fr 1fr')
        .columnsGap(8)
        .rowsGap(12)
        .padding({ top: 12, bottom: 12 })
        .height(124)
        .backgroundColor(Color.White)
        .borderRadius(24)
        Text('列表')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .width('100%')
          .margin({ top: 12 })
        //第二个网格布局
        Grid() {
          ForEach(mainViewModel.getSecondGridData(), (secondItem: ItemData) => {
            GridItem() {
              Column() {
                Text(secondItem.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                Text(secondItem.others)
                  .margin({ top: 4 })
                  .fontSize(12)
                  .fontColor($r('app.color.home_grid_fontColor'))
              }
              .alignItems(HorizontalAlign.Start)
            }
            .padding({ top: 8, left: 8 })
            .borderRadius(12)
            .align(Alignment.TopStart)
            .backgroundImage(secondItem.img)
            .backgroundImageSize(ImageSize.Cover)
            .width('100%')
            .height('100%')
          }, (secondItem: ItemData) => JSON.stringify(secondItem))
        }
        .width('100%')
        .height(260)
        .columnsTemplate('1fr 1fr')
        .rowsTemplate('1fr 1fr')
        .columnsGap(8)
        .rowsGap(12)
        .margin({ bottom: 55 })
      }
    }
    .height('100%')
  }
}


这里注意一下我将之前写过的一些代码省略了,所以这里你就不要复制粘贴了,其实网格列表和普通列表在数据渲染的方式上一样,只不过网格列表有一些其他的属性,我们需要了解。

  • columnsTemplate:string类型,设置当前网格布局列的数量,不设置时默认1列。例如, ‘1fr 1fr 1fr 1fr’ 是将父组件分4列,将父组件允许的宽分为4等份,第一列占1份,第二列占1份,第三列占1份,第四列占1份。设置为’0fr’时,该列的列宽为0,不显示GridItem。设置为其他非法值时,GridItem显示为固定1列。
  • rowsTemplate:string类型,设置当前网格布局行的数量,不设置时默认1行。例如,‘1fr 1fr’是将父组件分两行,将父组件允许的高分为2等份,第一行占1份,第二行占1份,设置为’0fr’,则这一行的行宽为0,这一行GridItem不显示。设置为其他非法值,按固定1行处理。
  • columnsGap:Length类型,设置列与列的间距。默认值:0,设置为小于0的值时,按默认值显示。
  • rowsGap:Length类型,设置行与行的间距。默认值:0,设置为小于0的值时,按默认值显示。


其余的属性就没有什么好说的,下面我们再预览一下Index,如下图所示:

1a1c8e0e88854602b708bf2d271f720f.png

此时你点击我的,可以看到什么也没有,下面我们来写我的。


五、我的


首先我们看一下我的页面的图

738066cc92e745e59bde2fa0a4395dbc.png

内容同样是呈纵向摆放的,上面是个人信息,中间这里是一个功能列表,最下面是退出按钮,下面我们首先提供列表的数据,在IndexViewModel中写一个函数,代码如下所示:

  /**
   * 获取设置列表数据
   */
  getSettingListData(): Array<ItemData> {
    let settingListData: ItemData[] = [
      new ItemData('推送通知', $r('app.media.news'), '开关'),
      new ItemData('数据管理', $r('app.media.data'), null),
      new ItemData('菜单设置', $r('app.media.menu'), null),
      new ItemData('关于', $r('app.media.about'), null),
      new ItemData('清除缓存', $r('app.media.storage'), null),
      new ItemData('隐私协议', $r('app.media.privacy'), null)
    ]
    return settingListData
  }


然后我们在view包下先建一个Mine.ets,代码如下所示:

import router from '@ohos.router';
import promptAction from '@ohos.promptAction';
import ItemData from '../bean/ItemData';
import mainViewModel from '../viewmodel/IndexViewModel';
/**
 * 我的
 */
@Component
export default struct Mine {
  @Builder settingCell(item: ItemData) {
    Row() {
      Row({ space: 12 }) {
        Image(item.img)
          .width(22)
          .height(22)
        Text(item.title)
          .fontSize(16)
      }
      // 设置功能item最右侧的功能项
      if (item.others === null) {
        //可以进入下一级页面
        Image($r('app.media.right_grey'))
          .width(12)
          .height(24)
      } else {
        //开关
        Toggle({ type: ToggleType.Switch, isOn: false })
      }
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .width('100%')
    .padding({
      left: 8,
      right: 22
    })
  }
  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column() {
          Text('我的')
            .fontWeight(FontWeight.Medium)
            .fontSize(24)
            .margin({ top: 12 })
            .padding({ left: 12 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        // 个人信息
        Row() {
          Image($r('app.media.account'))
            .width(48)
            .height(48)
          Column() {
            Text('李先生')
              .fontSize(20)
            Text('lonelyxxx@qq.com')
              .fontSize(12)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 24 })
        }
        .margin({ top: 24 })
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .height(96)
        .backgroundColor(Color.White)
        .padding({ left: 24 })
        .borderRadius(16)
        // 功能列表
        List() {
          ForEach(mainViewModel.getSettingListData(), (item: ItemData) => {
            ListItem() {
              //构建每一个item
              this.settingCell(item)
            }
            .height(48)
          }, (item: ItemData) => JSON.stringify(item))
        }
        .backgroundColor(Color.White)
        .width('100%')
        .height('42%')
        // 为列表增加分隔线
        .divider({
          strokeWidth: 1,
          color: Color.Grey,
          startMargin: 42,
          endMargin: 42
        })
        .borderRadius(16)
        .padding({ top: 4, bottom: 4 })
        Blank()
        Button('退出登录', { type: ButtonType.Capsule })
          .width('90%')
          .height(40)
          .fontSize(16)
          .fontColor($r('app.color.setting_button_fontColor'))
          .fontWeight(FontWeight.Medium)
          .backgroundColor($r('app.color.setting_button_backgroundColor'))
          .margin({ bottom: 55 })
          .onClick(() => {
            promptAction.showToast({ message: '退出登录' })
            router.replaceUrl({ url: 'pages/Login' })
          })
      }
      .height('100%')
    }
  }
}


这个代码乍一看很多,下面我们来分析一下,从上往下来,首先是标题和个人信息,这部分就是UI效果,没有什么好说的,然后最关键的功能列表,这里通过@Builder 来装饰settingCell()函数。通过item的other来判断是否需要显示不同的效果,代码如下所示:

    if (item.others === null) {
        //可以进入下一级页面
        Image($r('app.media.right_grey'))
          .width(12)
          .height(24)
      } else {
        //开关
        Toggle({ type: ToggleType.Switch, isOn: false })
      }


为null就是一个向右的图标,不为null就是一个开关,默认为false。中间的列表加载就没有什么好说二的,最后的退出登录按钮点击之后就会调用router.replaceUrl({ url: 'pages/Login' }),返回到登录页面,这里使用的是replaceUrl,用应用内的某个页面替换当前页面,并销毁被替换的页面。

下面我们通过Index预览一下看看效果:

26edb5f65920449b96f9547e6d6f08c0.png

① 带参数跳转

  现在我们登录后的账号并没有其他作用,我们可以把账号替换为李先生,首先我们需要修改登录按钮点击事件的代码,如下所示:

    router.replaceUrl({
          url: 'pages/Index',
          params: {
            account: this.account
          }
        });


就是在跳转页面的时候添加一个params属性,然后放入键和值,然后我们在Mine组件中增加一行代码:

  //接收传递过来的参数
  @State account: string = router.getParams()?.['account'];


这样就能拿到传递的参数值,然后设置到Text中即可。

d750773264cb44b993d75bdd5ed36cd2.png

下面运行一下看看效果

9e747a440d4f428d8354d537d54d08d6.gif

  本文就到这里了,鸿蒙提供的一些学习资料是很全面的,通过阅读加上实操过程中的测试可以很快上手应用开发。


六、源码


如果对你有所帮助的话,不妨 StarFork,山高水长,后会有期~

源码地址:MyCenter

相关文章
|
3月前
|
存储 消息中间件 容器
当一个 Pod 中包含多个容器时,容器间共享一些重要的资源和环境,这使得它们能够更有效地协同工作和交互。
当一个 Pod 中包含多个容器时,容器间共享一些重要的资源和环境,这使得它们能够更有效地协同工作和交互。
|
3月前
|
缓存 开发者 Docker
Dockerfile是Docker容器化过程中的核心组件,它允许开发者以一种可重复、可移植的方式自动化地构建Docker镜像
【8月更文挑战第19天】Dockerfile是构建Docker镜像的脚本文件,含一系列指令定义镜像构建步骤。每条大写指令后跟至少一个参数,按序执行,每执行一条指令即生成新的镜像层。常用指令包括:FROM指定基础镜像;RUN执行构建命令;EXPOSE开放端口;CMD指定容器启动行为等。优化策略涉及减少镜像层数、选择轻量基础镜像、利用缓存及清理冗余文件。示例:基于Python应用的Dockerfile包括设置工作目录、复制文件、安装依赖等步骤。掌握Dockerfile有助于高效自动化构建镜像,加速应用部署。
34 1
|
3月前
|
前端开发 JavaScript 数据处理
React 中展示组件和容器组件
【8月更文挑战第31天】
75 0
|
3月前
|
域名解析 Kubernetes 负载均衡
在K8S中,外部访问容器服务,比如说提供了一个域名,链路怎么走?数据经过哪些组件?
在K8S中,外部访问容器服务,比如说提供了一个域名,链路怎么走?数据经过哪些组件?
|
4月前
|
JavaScript 前端开发 容器
vue组件封装——固定宽高比的容器(2种方法:纯CSS实现 + JS实现)
vue组件封装——固定宽高比的容器(2种方法:纯CSS实现 + JS实现)
189 2
|
5月前
|
监控 Linux 数据处理
Linux中的nsenter命令:深入容器内部,实现无缝交互
`nsenter`是Linux工具,用于进入容器的命名空间,实现与容器内环境的交互。它基于Linux内核的命名空间功能,支持网络、PID等多类型隔离。`nsenter`允许在不停止容器的情况下调试和操作,如 `-t` 指定PID进入命名空间,`-n` 进入网络命名空间。示例包括使用`nsenter`查看容器进程或网络配置。使用时注意目标进程状态,理解命名空间类型,并谨慎操作。
|
4月前
|
容器
Could not autowire No beans of ‘UserSerice,这样的bug,主要是idea检测到你没有往页面中,没有往容器中注入一个userService,容器注入UserSe
Could not autowire No beans of ‘UserSerice,这样的bug,主要是idea检测到你没有往页面中,没有往容器中注入一个userService,容器注入UserSe
|
4月前
|
Java 数据安全/隐私保护 容器
Java详解:GUI容器组件 | 功能组件
Java详解:GUI容器组件 | 功能组件
|
4月前
|
Java 容器
Java详解:GUI图形用户界面设计—容器组件及面板布局方式
Java详解:GUI图形用户界面设计—容器组件及面板布局方式
|
5月前
|
前端开发 程序员 UED
全面解析layui:掌握基础知识与实用技能(1. 核心组件与模块 2. 布局与容器 3. 弹出层与提示框;1. 数据表格与数据表单 2. 表单验证与提交 3. 图片轮播与导航菜单)
全面解析layui:掌握基础知识与实用技能(1. 核心组件与模块 2. 布局与容器 3. 弹出层与提示框;1. 数据表格与数据表单 2. 表单验证与提交 3. 图片轮播与导航菜单)
78 0