OpenHarmony应用开发实战:登录页UI设计与实现(ArkUI篇)

前置条件

  • 编译器版本 DevEco Studio devecostudio-windows-4.1.0.400
  •  open Harmony Stage模型基于open HarmonySDK-API9
  • 使用stage模型的应用,需要在module.json5配置文件中声明网络权限
  •     //使用stage模型的应用,需要在module.json5配置文件中声明网络权限
        "requestPermissions": [{
          "name": 'ohos.permission.INTERNET',
          "usedScene": {
            "abilities": [
              "EntryAbility"
            ],
            "when": "always"
          }
        }]

1.基础架构

1.1 核心组件(LoginPage)

  • LoginPage 是整个登录页面的核心容器组件,负责承载页面的所有功能模块,并进行整体布局和交互管理。

  • @Entry
    @Component
    struct LoginPage {
    
    build(){
    
    }
    
    }

1.2 状态管理模块

状态管理模块是登录页面的关键部分,负责管理和维护页面的状态数据。它分为两个主要部分:

  • 本地状态:用于管理用户输入的数据,例如登录表单中的用户名和密码。这些数据仅在页面会话期间有效,页面关闭后不会保留。

  • 持久化状态:用于存储认证相关的数据,如登录令牌(token)。这些数据通过UserSession、

    PreferencesUtil持久化存储,即使页面关闭或设备重启,用户登录状态也能被保留。

通过这种状态管理方式,页面能够灵活处理用户的临时操作数据和长期存储数据,确保登录功能的稳定性和用户体验。

1.3 UI组件模块

UI组件模块是登录页面的可视化部分,按照视觉层次划分为三个主要区域:

  • 顶部区域:包括应用标识(Logo)和欢迎语,用于展示应用的信息和欢迎提示。

  • 表单区域:是用户进行登录操作的核心部分,包含输入框组和按钮组。输入框组支持带图标的文本输入框,用于输入用户名和密码;按钮组包含登录按钮和注册按钮,用户可以通过点击这些按钮进行登录或注册操作。

  • 底部区域:可以包含版权信息等,用于展示页面的版权归属和其他相关信息。

1.4 交互逻辑模块

交互逻辑模块是登录页面的“大脑”,负责处理页面的交互行为,包括:

  • 网络请求处理:通过调用登录API和注册API,与后端服务器进行通信,完成用户的登录和注册操作。

  • 数据持久化存储:使用 PreferencesUtilUserSession 管理用户数据的持久化存储,确保用户登录状态的持久性。

  • 页面导航控制:通过 router.replaceUrl 方法实现页面的跳转和路由管理,例如在用户登录成功后跳转到主页。

2.页面基础架构

2.1 组件与接口定义

首先定义了两个关键接口来管理用户数据:

interface User {
  username: string;
  password: string;
  token: string;
}

interface Params {
  data: string,
  id: number,
  username: string,
  password: string,
  role: string,
}

1.2 组件状态管理

使用装饰器管理组件状态:

@Entry
@Component
struct LoginPage {
  @State user: User = { // 用户登录信息
    username: 'admin002',
    password: '123456',
    token: ''
  }
  @State UserInfo: Params = { // 用户详细信息
    data: '',
    id: 0,
    username: '',
    password: '',
    role: '',
  }
}

3.登录页UI实现

 3.1 自定义样式方法

        在open Harmony移动应用开发中,样式(Styles)和扩展(Extend)是用于定义和复用UI组件样式的重要工具。

        样式(Styles)

        样式(Styles)用于定义UI组件外观的一种机制。它允许开发者将一组样式规则封装起来,并通过一个唯一的名称进行引用。样式可以应用于多种组件,从而实现样式的复用。

@Styles
function iconStyle() {
  .width(24)
  .height(24)
}
  • @Styles:这是一个装饰器,用于声明以下函数是一个样式定义。

  • iconStyle:这是样式函数的名称,它定义了一组样式规则。

  • .width(24).height(24):这两个方法分别设置了组件的宽度和高度,单位默认为像素(px)。

        通过这种方式,iconStyle 定义了一个固定大小为 24x24 的样式,可以应用于任何需要固定尺寸图标的组件。例如,你可以将这个样式应用于一个图标组件(Icon),使其宽度和高度均为 24 像素。

        扩展(Extend)

        扩展(Extend)用于扩展组件行为和样式的机制。通过扩展,开发者可以为现有的组件添加新的样式规则,而无需修改组件的原始代码。扩展不仅可以设置组件的外观属性,还可以设置其行为属性。

@Extend(TextInput)
function inputStyle() {
  .height(40)
  .layoutWeight(1)
  .fontSize(14)
  .backgroundColor(Color.Transparent)
}
  • @Extend(TextInput):这是一个装饰器,用于声明以下函数是一个扩展定义,并且这个扩展是针对 TextInput 组件的。

  • inputStyle:这是扩展函数的名称,它定义了一组应用于 TextInput 组件的样式规则。

  • .height(40):设置输入框的高度为 40 像素。

  • .layoutWeight(1):设置输入框的布局权重为 1,这在布局中用于控制组件的伸缩比例。

  • .fontSize(14):设置输入框的字体大小为 14 像素。

  • .backgroundColor(Color.Transparent):设置输入框的背景颜色为透明。

 3.2 UI组件

顶部区域

        

Column() {
  Image($r("app.media.logo"))  // 品牌Logo
    .width('45%')              // 响应式宽度
  Text('Hi There!')            // 欢迎标语
    .fontSize(30)
    .fontColor('#fff6f6f6')
  Text('XX系统')          // 系统名称
    .fontSize(35)
    .fontColor('#fff6f6f6')
}
  • 采用垂直布局(Column)实现信息层级展示
  • Logo使用自适应宽度(45%)保持比例
  • 双行文字形成视觉对比(英文+中文)

表单区域 

Column() {
  // 用户名输入组
  Row() {
    Image($r("app.media.user"))  // 图标
    TextInput({ placeholder: '请输入用户名' })
      .inputStyle()  // 统一样式
  }
  
  Divider().color(Color.Black)  // 分割线
  
  // 密码输入组
  Row() {
    Image($r("app.media.password"))
    TextInput({ placeholder: '请输入密码' })
  }
  
  // 双按钮布局
  Row() {
    Button('立即登录')  // 主操作按钮
      .width('40%')
    Blank().width('10%')  // 间距控制
    Button('立即注册')  // 次要操作按钮
      .width('40%')
  }
  
  // 协议文本
  Row() {
    Text('登录即表示已同意')
    Text('《用户使用协议》')  // 可点击文本
  }
}.formBgStyle()  // 应用表单样式
  1. ​输入框组​​:

    • 图标+输入框的水平布局
    • 使用Divider增强视觉分隔
    • 统一的inputStyle保证一致性
  2. ​按钮组​​:

    • 4:1:4的黄金比例布局
    • 主次按钮相同宽度保持平衡
  3. ​其他信息文本​

    • 10px小字号避免喧宾夺主

底部信息区域

Column() {
  Blank().height("5%")  // 留白缓冲
  Text('Developed By LY_2025')  // 版权信息
    .fontSize(12)
    .fontColor('#546B9D')  // 深灰色降低存在感
}
  • 通过Blank组件控制间距
  • 小字号(12px)深色文字保持低调
  • 居中对齐保持视觉平衡

 样式系统设计

// 背景样式
@Styles function loginBgStyle() {
  .width('100%')
  .height('100%')
  .padding(20)  // 安全边距
}

// 表单容器样式
@Styles function formBgStyle() {
  .backgroundColor('rgba(255,255,255,0.75)')  // 半透明白色
  .borderRadius(20)  // 圆角设计
  .padding(30)  // 内边距
}

// 输入框扩展样式
@Extend(TextInput) function inputStyle() {
  .height(40)  // 统一高度
  .backgroundColor(Color.Transparent)  // 透明背景
}

4.交互逻辑

4.1 登录功能

Button('立即登录')
.onClick(() => {
  login(this.user.username, this.user.password).then(res => {
    if (res.code == 200) {
      // 保存token,用于用户信息持久化,即保持登录
      PreferencesUtil.saveToken(res.data.token)
      // 更新用户信息
      this.UserInfo = {
        data: res.data.token,
        id: res.data.id,
        username: res.data.username,
        password: res.data.password,
        role: res.data.role
      }
      //全局存储token,用户后续API获取信息验证
      UserSession.saveUserInfo(this.UserInfo)
      // 跳转首页
      router.replaceUrl({
        url: 'pages/Index',
        params: this.UserInfo
      })
    } else {
      AlertDialog.show({ message: res.msg })
    }
  })
})
  • login 是一个异步函数,用于向后端发送登录请求。

  • this.user.usernamethis.user.password 是用户输入的用户名和密码。

  • 使用.then(res => { ... })处理异步返回的结果。

  • 保存Token:使用PreferencesUtil.saveToken(res.data.token)将登录返回的Token保存到本地存储中。Token用于后续的请求认证。

  • 更新用户信息:将用户的基本信息(如用户名、密码、角色等)存储到this.UserInfo中,并通过UserSession.saveUserInfo(this.UserInfo)持久化用户信息。

  • 页面跳转:使用router.replaceUrl方法跳转到首页(pages/Index),并传递用户信息作为参数。

  • 如果登录失败(res.code != 200),通过AlertDialog.show弹出一个对话框,显示错误信息(res.msg)。

1. 异步请求处理

        登录操作通常涉及网络请求,因此需要使用异步处理。login函数返回一个Promise对象,通过.then方法处理成功响应,通过.catch可以处理网络请求的错误。

2. 状态管理
  • 本地状态this.UserInfo用于存储用户信息,方便在当前页面或其他页面中使用。

  • 持久化状态:使用PreferencesUtilUserSession将用户信息和Token持久化存储,确保用户登录状态在应用重启后仍然有效。

3. 页面导航

        使用router.replaceUrl方法实现页面跳转。该方法可以替换当前页面的路由,避免返回到登录页面,从而提升用户体验。

4. 错误处理

        通过AlertDialog弹出对话框显示错误信息,这是一种简单直观的错误提示方式。在实际开发中,可以根据需要扩展错误处理逻辑,例如记录日志或提供重试按钮。

 5.完整代码

        预览效果

完整代码

import router from '@ohos.router'
import { login, register } from '../Http/HttpUser'
import PreferencesUtil from '../Utils/Prefereces'
import { UserSession } from '../Utils/UserInfo';

interface User {
  username: string;
  password: string;
  token: string;
}

interface Params {
  data: string,
  id: number,
  username: string,
  password: string,
  role: string,
}

@Entry
@Component
struct LoginPage {
  //判断设备大小
  @State currentBreakpoint: string = 'sm'
  @State user: User = {
    username: 'admin002',
    password: '123456',
    token: ''
  }
  @State UserInfo: Params = {
    data: '',
    id: 0,
    username: '',
    password: '',
    role: '',
  }
  @StorageLink('currentTabIndex') currentTabIndex: number = 0;

  build() {
    GridRow({ columns: { sm: 4, md: 8, lg: 12 } }) {
      GridCol({ span: { sm: 4, md: 8, lg: 12 } }) {
        List() {
          ListItem() {
            Column() {
              Column() {
                Image($r("app.media.logo"))
                  .width('45%')
                Text('Hi There!')
                  .fontSize(30)
                  .fontColor('#fff6f6f6')
                  .margin(10)
                Text('XX系统')
                  .fontSize(35)
                  .fontColor('#fff6f6f6')
                  .margin(10)
              }

              Blank().height("7%")
              Column() {
                Row() {
                  Image($r("app.media.user"))
                    .iconStyle()
                  TextInput({ placeholder: '请输入用户名', text: this.user.username })
                    .inputStyle()
                    .onChange((value) => {
                      this.user.username = value;
                    })
                }.margin({ top: 30 })

                Divider()
                  .color(Color.Black)
                Row() {
                  Image($r("app.media.password"))
                    .iconStyle()
                  TextInput({ placeholder: '请输入密码', text: this.user.password })
                    .inputStyle()
                    .onChange((value) => {
                      this.user.password = value;
                    })
                }.margin({ top: 20 })

                Divider()
                  .color(Color.Black)
                Row() {
                  Button('立即登录')
                    .width('40%')
                    .backgroundColor('#DC6FC38F')
                    .margin({ top: 50 })
                    .onClick(() => {
                      login(this.user.username, this.user.password).then(res => {
                        if (res.code == 200) {
                          console.info('Result:' + res.data.token);
                          //存储token
                          PreferencesUtil.saveToken(res.data.token)
                          this.UserInfo.data = res.data.token
                          this.UserInfo.id = res.data.id
                          this.UserInfo.username = res.data.username
                          this.UserInfo.password = res.data.password
                          this.UserInfo.role = res.data.role
                          UserSession.saveUserInfo(this.UserInfo)
                          setTimeout(() => {
                            this.user.token = res.data.token
                            router.replaceUrl({
                              url: 'pages/Index',
                              params: {
                                id: res.data.id,
                                username: res.data.username,
                                password: res.data.password,
                                role: res.data.role,
                                data: this.user.token
                              }
                            })
                          }, 500)
                        } else {
                          AlertDialog.show({
                            title: '提示',
                            message: res.msg
                          })
                        }
                      })
                    })
                  Blank().width('10%')
                  Button('立即注册')
                    .width('40%')
                    .backgroundColor('#DC6FC38F')
                    .margin({ top: 50 })
                    .onClick(() => {
                      register(this.user.username, this.user.password).then(res => {
                        if (res.code == 200) {
                          console.info('Result:' + res.data);
                          setTimeout(() => {
                            AlertDialog.show({
                              title: '提示',
                              message: res.msg
                            })
                            router.replaceUrl({
                              url: 'pages/Login',
                            })
                          }, 500)
                        } else {
                          AlertDialog.show({
                            title: '提示',
                            message: res.msg
                          })
                        }
                      })
                    })
                }

                Row() {
                  Text('登录即表示已同意')
                    .fontSize(10)
                    .fontColor('#546B9D')
                  Text('《用户使用协议》')
                    .fontSize(10)
                    .fontColor('#00B3FF')
                }.margin({ top: 20 })
              }.formBgStyle()

              Blank().height("5%")
              Text('Developed By LY_2025')
                .fontSize(12)
                .fontColor('#546B9D')
                .margin(10)
            }
          }
        }
        .width('100%')
        .height('90%')
        .loginBgStyle()
        .linearGradient(
          {
            angle: 180, //设置角度之后,direction不生效
            colors: [['#dc6fc38f', 0.1], ["#c960bf84", 0.6], ["#d76bde93", 1]]
          })
      }
    }.zIndex(-1)
    .onBreakpointChange((breakpoint: string) => {
      this.currentBreakpoint = breakpoint
    })
  }
}

@Styles
function loginBgStyle() {
  .width('100%')
  .height('100%')
  //.backgroundImage($r("app.media.img_login_bg"))
  // .backgroundImageSize({ width: '100%', height: '100%' })
  .padding({
    top: 30,
    bottom: 30,
    left: 20,
    right: 20
  })
}

@Styles
function formBgStyle() {
  .backgroundColor('rgba(255, 255, 255, 0.75)')
  .padding(30)
  .borderRadius(20)
}

@Styles
function iconStyle() {
  .width(24)
  .height(24)
}

@Extend(TextInput)
function inputStyle() {
  .height(40)
  .layoutWeight(1)
  .fontSize(14)
  .backgroundColor(Color.Transparent)
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值