HarmonyOS NEXT - 电商App实例四(登录界面)
登录界面是用户进入App的第一步,因此需要简洁明了,同时保持品牌风格的一致性。如:顶部区域为品牌LOGO展示,增加品牌识别度;中间区域为登录表单,包含输入框和按钮;底部区域为其他登录方式、注册入口和忘记密码相关链接。
在HarmonyOS中,使用ArkTS-UI框架完成登录界面的设计,会使用到Text组件、Textinput组件、Button组件、Image组件、Link组件、Row和Column布局容器等。数据交互方面,使用@State装饰器记录用户名和密码的表单数据状态,按钮事件和输入框事件的处理函数,则使用到onClick和onChange,确保用户操作能够触发相应的逻辑。
一、界面设计
在HarmonyOS中使用ArkTS-UI设计登录界面,将使用以下组件:
1.1 Row和Column布局容器
使用Row和Column容器完成登录表单的布局。通过配置Row容器的padding属性,使App容器四周留出20像素间距,通过配置Column容器的justifyContent,使内容水平和垂直居中。
示例代码如下:
@Entry
@Component
struct Login {
build() {
RelativeContainer() {
Row(){
Column(){
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.padding(20)
}
.height('100%')
.width('100%')
}
}
1.2 Image组件
使用Image组件用于显示品牌Logo。代码如下:
@Entry
@Component
struct Login {
build() {
RelativeContainer() {
Row(){
Column(){
// 添加Logo图标
Image($rawfile('logo.png'))
.width(80)
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.padding(20)
}
.height('100%')
.width('100%')
}
}
页面效果如下图:
1.3 Text组件
使用Text组件,用于显示Logo文本信息,如登录标题、提示等。示例代码如下:
@Entry
@Component
struct Login {
build() {
RelativeContainer() {
Row(){
Column(){
// 添加Logo图标
Image($rawfile('logo.png'))
.width(80)
// 添加标题
Text('欢迎登录XXX平台')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ top: 15, bottom: 15})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.padding(20)
}
.height('100%')
.width('100%')
}
}
页面效果如下图:
1.4 TextInput组件
使用TextInput组件,用于输入用户名或手机号、密码等。同时,使用Column容器(垂直布局)将表单内容包裹起来,并配置space为20,使其内部元素垂直布局元素间距为20。示例代码如下:
@Entry
@Component
struct Login {
build() {
RelativeContainer() {
Row({ space: 20 }){
Column(){
// 添加Logo图标
Image($rawfile('logo.png'))
.width(80)
// 添加标题
Text('欢迎登录XXX平台')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ top: 15, bottom: 15})
// 表单输入框
Column({ space: 20 }){
TextInput({ placeholder: '请输入用户名/手机号' })
TextInput({ placeholder: '请输入密码' }).type(InputType.Password)
}.padding({ top: 20, bottom: 50 })
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.padding(20)
}
.height('100%')
.width('100%')
}
}
页面效果如下图:
1.5 Button组件
使用Button组件,用于登录、注册或忘记密码等功能。示例代码如下:
@Entry
@Component
struct Login {
build() {
RelativeContainer() {
Row({ space: 20 }){
Column(){
// 添加Logo图标
Image($rawfile('logo.png'))
.width(80)
// 添加标题
Text('欢迎登录XXX平台')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ top: 15, bottom: 15})
// 表单输入框
Column({ space: 20 }){
TextInput({ placeholder: '请输入用户名/手机号' })
TextInput({ placeholder: '请输入密码' }).type(InputType.Password)
}.padding({ top: 20, bottom: 50 })
// 登录按钮
Button('登 录', { type: ButtonType.Capsule })
.width('100%')
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.padding(20)
}
.height('100%')
.width('100%')
}
}
页面效果如下图:
1.6 链接文本
使用Link组件,用于注册、忘记密码的链接。用Row容器(水平布局)将文本链接包裹起来,并且文本组件之间的间距设置为20。示例代码如下:
@Entry
@Component
struct Login {
build() {
RelativeContainer() {
Row({ space: 20 }){
Column(){
// 添加Logo图标
Image($rawfile('logo.png'))
.width(80)
// 添加标题
Text('欢迎登录XXX平台')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ top: 15, bottom: 15})
// 表单输入框
Column({ space: 20 }){
TextInput({ placeholder: '请输入用户名/手机号' })
TextInput({ placeholder: '请输入密码' }).type(InputType.Password)
}.padding({ top: 20, bottom: 80 })
// 登录按钮
Button('登 录', { type: ButtonType.Capsule })
.width('100%')
// 添加 忘记密码 和 注册
Row({ space: 20 }){
Text('忘记密码?').fontColor('#1495E7')
Text('注册').fontColor('#FF0000')
}.width('100%')
.justifyContent(FlexAlign.End)
.padding({ top: 15, right: 10, bottom: 50})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.padding(20)
}
.height('100%')
.width('100%')
}
}
页面效果如下图:
通过以上组件,就已设计出一个简洁且功能完善的登录界面。
二、界面交互功能
界面元素已设计完毕,现在则需要设计交互功能,当用户输入内容时,需将填写内容实时赋值给状态变量;如在未填任何内容时,用户点击“登录”按钮,则弹出提示框提示用户。
2.1 状态管理
使用@State装饰器管理登录表单的状态,例如用户和密码。示例代码如下:
@Entry
@Component
struct Login {
@State username: string = ''; // 用户名
@State password: string = ''; // 密码
// 略...
}
2.2 更新表单数据
在输入框上添加onChange事件,当文本内容被修改后,实时将回调函数中内容赋值给状态变量。示例代码如下:
// 表单输入框
Column({ space: 20 }){
TextInput({ placeholder: '请输入用户名/手机号', text: this.username })
.onChange((val: string) => {
this.username = val
})
TextInput({ placeholder: '请输入密码', text: this.password })
.type(InputType.Password)
.onChange((val: string) => {
this.password = val
})
2.3 登录按钮
在登录按钮上添加onClick事件,当用户点击时,校验用户名/手机号,密码是否正确;如信息有误,弹出信息提示用户;如信息正确,执行登录操作。示例代码如下:
// 表单输入框
Column({ space: 20 }){
TextInput({ placeholder: '请输入用户名/手机号', text: this.username })
.onChange((val: string) => {
this.username = val
})
TextInput({ placeholder: '请输入密码', text: this.password })
.type(InputType.Password)
.onChange((val: string) => {
this.password = val
})
}.padding({ top: 20, bottom: 80 })
// 登录按钮
Button('登 录', { type: ButtonType.Capsule })
.width('100%')
.onClick(() => {
// do something...
console.log(this.username, this.password)
})
2.4 弹框组件
在表单校验前,先定义showMsg函数,用于信息提示功能。提示框使用的是HarmonyOS内置函数promptAction,示例代码如下:
/**
* 显示信息
* @param text
*/
showMsg(text: string = '') {
promptAction.showToast({
message: text,
duration: 1500,
alignment: Alignment.Center
})
}
AlertDialog的主要参数说明:
名称 | 说明 |
---|---|
message | 弹窗内容。 |
duration | 提示框显示的时长,单位为毫秒。超出该时长后,提示框会自动关闭。 |
alignment | 提示框的显示位置,支持多种对齐方式(如Alignment.Center、Alignment.Top等)。 |
backgroundColor | 提示框的背景颜色。 |
textColor | 提示框文字的颜色。 |
2.5 信息校验
在正式将登录表单数据发送给后台之前,前端必须先做基础校验,验证数据符合要求后,再执行发送请求。在登录界面,定义validateForm函数,用于校验用户输入的表单数据。代码如下:
/**
* 校验表单
*/
validateForm(){
if (!this.username || !this.password) {
this.showMsg('请输入用户名或名称')
return;
} else if (this.password.length < 6) {
this.showMsg('密码不能小于6位')
return;
}
// 校验通过,提交数据
console.log(this.username, this.password)
}
此时,如果用户什么也没填写,点击“登录”按钮,会提示用户输入相关内容。如下图:
三、数据交互
登录界面必须要与后台完成数据交互,才能最终实现登录的功能。通常通过HTTP请求来完成,这里就直接使用HarmonyOS提供的http模块来实现这步。
在前面两篇中,已讲解过http和axios请求模块,根据自身喜好选择一个来完成项目的数据请求即可。
地址一:HarmonyOS开发 - 电商App实例二( 网络请求http)-CSDN博客
地址二:HarmonyOS开发 - 电商App实例三( 网络请求axios)-CSDN博客
3.1 定义标准接口返回类型
打开项目的types目录下的http.d.ts文件,在里面定义一个标准接口返回类型。
代码如下:
/**
* 接口标准返回格式
*/
interface standardInterfaceResult {
code: number,
data: any,
msg: string
}
3.2 定义登录API函数
如下图,在项目的api目录,创建login.ts文件,用于定义登录、注册、忘记密码等相关api的请求函数。
示例代码如下:
import { standardInterfaceResult } from '../types/http'
import { httpRequest } from '../utils/request'
/**
* 登录Api函数
*/
export const login = async (data) => {
return await httpRequest.post<standardInterfaceResult>('/login.php', data)
}
3.3 登录请求
将登录定义的API函数login()引入到登录界面,当用户信息输入校验正确时,执行login()函数并得到响应后,根据后台返回结果做出相应操作。代码如下:
import { login } from '../api/login'
import router from '@ohos.router';
@Entry
@Component
struct Login {
@State username: string = ''; // 用户名
@State password: string = ''; // 密码
/**
* 显示信息
* @param text
*/
showMsg(text: string = '') {
AlertDialog.show({
title: '提示',
message: text
})
}
/**
* 校验表单
*/
validateForm(){
if (!this.username || !this.password) {
this.showMsg('请输入用户名或名称')
return;
} else if (this.password.length < 6) {
this.showMsg('密码不能小于6位')
return;
}
// 校验通过,提交数据
login({
username: this.username,
password: this.password
}).then(res => {
this.showMsg(res.msg)
// code 为200时,跳转到指定界面
if (res.code == 200) {
// 跳转到首页
setTimeout(() => {
router.pushUrl({
url: '/pages/Index'
})
}, 1200)
}
})
}
build() {
RelativeContainer() {
Row({ space: 20 }){
Column(){
// 略...
// 表单输入框
Column({ space: 20 }){
TextInput({ placeholder: '请输入用户名/手机号', text: this.username })
.onChange((val: string) => {
this.username = val
})
TextInput({ placeholder: '请输入密码', text: this.password })
.type(InputType.Password)
.onChange((val: string) => {
this.password = val
})
}.padding({ top: 20, bottom: 80 })
// 登录按钮
Button('登 录', { type: ButtonType.Capsule })
.width('100%')
.onClick(() => {
// 校验表单
this.validateForm()
})
// 略...
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.padding(20)
}
.height('100%')
.width('100%')
}
}
此时,我们随便输入一个用户名(13233332222)和密码(12345),后台校验不通过,同样会返回相应错误信息,提示用户重新操作。如下图:
现在,我们输入正确的用户名(13200002222)和密码(123456),再来看下结果。如下图:
如下图,控制台输出登录接口的响应数据,其中包含了用户的信息和登录访问令牌。
3.4 本地信息存储
登录成功后,可以将登录接口响应的用户信息存储到本地,以便下次自动填充或保持登录状态;这里,可以使用dataPreference模块,将用户信息和访问令牌缓存在本地。
在整个项目中,如下单、订单信息、用户信息等相关数据,都需要在登录状态下才能访问;并且,这些需要权限的api请求,要在http的header配置中添加访问令牌(即登录成功时,返回的accesstoken数据)。
所以,我们要在utils目录中,定义一个localStorage.ts文件,用于全局存储用户和访问令牌数据,并有共享读取功能。之前写过几篇本地持久化文章,大家可以参考一下,地址:HarmonyOS开发 - 本地持久化之实现LocalStorage实例_鸿蒙开发本地缓存-CSDN博客。
3.4.1 localStorage.ts文件
代码如下:
import common from '@ohos.app.ability.common'
import preferences from '@ohos.data.preferences'
/**
* 判断字符串是否为JSON对象
*/
export const isJsonObject = (value: string) : boolean => {
try {
const parseStr = JSON.parse(value)
return 'object' === typeof parseStr && null !== parseStr
} catch (e) {
console.log('testTag', e)
return false
}
}
// 定义存储值类型
type valueType = string | number | boolean
// 定义json对象存储类型
type dataType = { value: valueType | object, expire: number }
/**
* 定义LocalStorage类
*/
export class LocalStorage {
private preference: preferences.Preferences // 用户首选项实例对象
// 定义初始化函数
initial(context: common.UIAbilityContext): void {
// 这里将UIAbility中应用上下文的moduleName作用为实例名称,即该项目的application
preferences.getPreferences(context, context.abilityInfo.moduleName).then(preference => {
this.preference = preference
console.log('testTag', 'success~')
}).catch(e => {
console.log('testTag error', e)
})
}
/**
* 定义增加函数
* @param key
* @param value
* @param expire
*/
put(key: string, value: valueType | object, expire?: Date): void {
// 定义存储Json格式对象
const data : dataType = {
value, // 存储内容
expire : (expire ? expire.getTime() : -1) // 如果失效时间存在,将其转换为时间戳,否则传入-1
}
let dataStr: string = '';
try {
dataStr = JSON.stringify(data) // 当数据转换成功,将其存储
console.log('testTag', dataStr)
} catch (e) {
console.log('testTag error', e)
return
}
this.preference.put(key, dataStr).then(() => this.preference.flush()).catch(e => {
console.log('testTag error', e)
})
}
/**
* 定义获取对应key数据
* @param key
*/
async getValue(key: string): Promise<valueType | object> {
// 首页判断key值是否存在,不存在返回空
if(!this.preference.has(key)) {
return Promise.resolve(null)
}
let value = (await this.preference.get(key, '')) as valueType
// 判断如果为字符串类型数据,并且为JSON对象格式数据,将其转换为对象
if('string' === typeof value && isJsonObject(value)) {
try {
const data: dataType = JSON.parse(value)
console.log('testTag', data.expire, Date.now(), data.expire < Date.now())
// 如果当前存储内容无时效性,或者在时效期内,都直接返回
if(data.expire === -1 || data.expire > Date.now()) {
return Promise.resolve(data.value)
}
// 如果已失效,将其信息删除
else {
this.preference.delete(key)
}
} catch (e) {
console.log('testTag error', e)
return Promise.resolve(null) // 如果转换出错,返回null
}
}
// 通过Promise异步回调将结果返回(如果内容不为JSON格式对象,或者过了时效期,返回null)
return Promise.resolve(null)
}
/**
* 更新数据
* @param key
* @param value
*/
async update(key: string, value: valueType){
try {
const preValue = await this.getValue(key)
if(preValue != value) {
this.put(key, value)
}
} catch (e) {
console.log('testTag error', e)
}
}
/**
* 定义移除函数
* @param key
*/
remove(key: string): void {
this.preference.delete(key).then(() => this.preference.flush()).catch(e => {
console.log('testTag error', e)
})
}
/**
* 定义清除所有数据函数
*/
clearAll(): void {
this.preference.clear().then(() => this.preference.flush()).catch(e => {
console.log('testTag error', e)
})
}
}
/**
* 实例LocalStorage
*/
const localStorage = new LocalStorage()
/**
* 导出localStorage单例对象
*/
export default localStorage as LocalStorage
功能参数说明 :
字段 | 说明 |
---|---|
initial | 初始化LocalStorage函数,在App打开时,需要将UIAbilityContext传入 |
put | 该方法用于添加缓存数据, key:缓存数据键名 value: 要缓存的数据 expire:需要缓存的时间(注意:是Date类型数据) |
getValue | 通过key获取缓存的数据 |
update | 通过key更新被缓存的数据 |
remove | 通过key移除指定的缓存数据 |
clearAll | 清空该实例context下的所有缓存数据 |
3.4.2 UIAbility中初始化
打开文件目录:src/main/ets/entryability/EntryAbility.ets,找到UIAbility中的onCreate函数,在其内部初始化。
代码如下:
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import LocalStorage from '../utils/localStorage'
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext().setColorMode(
ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
);
// 初始化LocalStorage
LocalStorage.initial(this.context)
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
}
// 略...
}
3.4.3 本地模拟器
注意,本地缓存使用的是@ohos.data.preferences,必须在本地模拟器中才能生效;在“预览”中无法缓存,故登录后获取不到缓存数据,所以必须在模拟器中演示和操作。如下图的步骤,打开本地模拟器:
3.4.4 定义获取信息函数
接下来,我们先在首页中,添加获取登录信息的方法;如果登录信息不存在,则跳转登录界面;如果存在,则显示登录信息。首页代码如下:
import LocalStorage from '../utils/localStorage';
import router from '@ohos.router';
// 定义用户数据类型
interface userInfo {
name: string,
avatar: string
}
@Entry
@Component
struct Index {
@State bannerMessage: string = '';
/**
* 获取用户信息
*/
async getUserInfo(){
const userInfo = await LocalStorage.getValue('userInfo') as userInfo
const accessToken = await LocalStorage.getValue('accessToken')
console.log('tag', userInfo, accessToken)
if (!userInfo || !accessToken) {
router.pushUrl({
url: 'pages/Login'
})
}
this.bannerMessage = `userInfo: ${userInfo.name} accessToken:${accessToken}`;
}
build() {
RelativeContainer() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
Text(this.bannerMessage)
Button('登录令牌').onClick(() => {
this.getUserInfo()
})
}.width('100%')
}
.height('100%')
.width('100%')
}
}
模拟器首页如下图:
点击“登录令牌”,查看控制台输出结果为null,表示用户信息未获取到,故跳转至登录界面。
3.4.5 缓存数据
上述工作准备完成后,就可以将登录接口返回的响应数据,通过封装的LocalStorage功能来缓存起来了。
在登录成功后,使用LocalStorage实例对象,将数据缓存一天。代码如下:
import { login } from '../api/login'
import router from '@ohos.router';
import { promptAction } from '@kit.ArkUI';
import LocalStorage from '../utils/localStorage'
@Entry
@Component
struct Login {
@State username: string = ''; // 用户名
@State password: string = ''; // 密码
/**
* 显示信息
* @param text
*/
showMsg(text: string = '') {
promptAction.showToast({
message: text,
duration: 1500,
alignment: Alignment.Center
})
}
/**
* 校验表单
*/
validateForm(){
if (!this.username || !this.password) {
this.showMsg('请输入用户名或名称')
return;
} else if (this.password.length < 6) {
this.showMsg('密码不能小于6位')
return;
}
console.log(this.username, this.password)
// 校验通过,提交数据
login({
username: this.username,
password: this.password
}).then(res => {
this.showMsg(res.msg)
console.log('res', JSON.stringify(res.data))
// code 为200时,跳转到指定界面
if (res.code == 200) {
const date = new Date()
date.setDate(date.getDate() + 1) // 从今天开始,往后缓存一天
// 缓存数据
LocalStorage.put('userInfo', res.data['userInfo'], date)
LocalStorage.put('accessToken', res.data['accessToken'], date)
// 跳转到首页
setTimeout(() => {
router.pushUrl({
url: 'pages/Index'
})
}, 1200)
}
}).finally(() => {
this.username = ''
this.password = ''
})
//
}
build() {
RelativeContainer() {
Row({ space: 20 }){
Column(){
// 添加Logo图标
Image($rawfile('logo.png'))
.width(80)
// 添加标题
Text('欢迎登录XXX平台')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ top: 15, bottom: 15})
// 表单输入框
Column({ space: 20 }){
TextInput({ placeholder: '请输入用户名/手机号', text: this.username })
.onChange((val: string) => {
this.username = val
})
TextInput({ placeholder: '请输入密码', text: this.password })
.type(InputType.Password)
.onChange((val: string) => {
this.password = val
})
}.padding({ top: 20, bottom: 80 })
// 登录按钮
Button('登 录', { type: ButtonType.Capsule })
.width('100%')
.onClick(() => {
// 校验表单
this.validateForm()
})
// 添加 忘记密码 和 注册
Row({ space: 20 }){
Text('忘记密码?').fontColor('#1495E7')
Text('注册').fontColor('#FF0000')
}.width('100%')
.justifyContent(FlexAlign.End)
.padding({ top: 15, right: 10, bottom: 50})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.padding(20)
}
.height('100%')
.width('100%')
}
}
当登录信息不存在时,跳转至登录界面,输入用户信息,完成登录操作。如下图:
点击“登录”,查看控制台输出。LocalStorage缓存数据控制正常输出信息,表示缓存成功。如下图:
3.4.6 获取用户信息
登录成功后,会自动跳转至首页,接下来我们重新获取用户信息,查看结果。如下图:
当点击“登录令牌”,用户名称和访问令牌则成功获取到,并显示在界面上。
这里,登录功能就讲完了,如果你有更好的方法,欢迎随时沟通交流!