前置条件
- 编译器版本 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,与后端服务器进行通信,完成用户的登录和注册操作。
-
数据持久化存储:使用
PreferencesUtil
和UserSession
管理用户数据的持久化存储,确保用户登录状态的持久性。 -
页面导航控制:通过
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() // 应用表单样式
输入框组:
- 图标+输入框的水平布局
- 使用Divider增强视觉分隔
- 统一的inputStyle保证一致性
按钮组:
- 4:1:4的黄金比例布局
- 主次按钮相同宽度保持平衡
其他信息文本:
- 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.username
和this.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
用于存储用户信息,方便在当前页面或其他页面中使用。 -
持久化状态:使用
PreferencesUtil
和UserSession
将用户信息和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)
}