登录界面是用户进入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作用为实例名称,即该项目的applicationpreferences.getPreferences(context, context.abilityInfo.moduleName).then(preference => {this.preference = preferenceconsole.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);// 初始化LocalStorageLocalStorage.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 userInfoconst 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 获取用户信息
登录成功后,会自动跳转至首页,接下来我们重新获取用户信息,查看结果。如下图:
当点击“登录令牌”,用户名称和访问令牌则成功获取到,并显示在界面上。
这里,登录功能就讲完了,如果你有更好的方法,欢迎随时沟通交流!