本案例基于窗口能力,实现验证码登录的场景,主要完成以下功能:
- 登录页面主窗口实现沉浸式。
- 输入用户名和密码后,拉起验证码校验子窗口。
- 验证码校验成功后,主窗口跳转到应用首页。
1. 案例效果截图
2. 案例运用到的知识点
- 核心知识点
- 主窗口:应用主窗口用于显示应用界面,会在“任务管理界面”显示。
- 子窗口:应用子窗口用于显示应用的弹窗、悬浮窗等辅助窗口。
- 沉浸式:指对状态栏、导航栏等系统窗口进行控制,减少状态栏导航栏等系统界面的突兀感,从而使用户获得最佳体验的能力。
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local/@Param/@Monitor
- 自定义组件和组件生命周期
- @Builder装饰器:自定义构建函数
- @Extend装饰器:定义扩展组件样式
- ForEach:循环渲染
- if/else:条件渲染
- 内置组件:Column/Row/Scroll/Swiper/Grid/Stack/Text/TextInput/Image/Button/Blank/Line/Rect
- 日志管理类的编写
- 常量与资源分类的访问
- 组件导航 (Navigation)
- 页面路由 (@ohos.router)
- MVVM模式
3. 代码结构
├──entry/src/main/ets // 代码区
│ ├──common
│ │ ├──constants
│ │ │ └──CommonConstants.ets // 公共常量类
│ │ └──utils
│ │ ├──GlobalContext.ets // 全局上下文
│ │ └──Logger.ets // 公共日志类
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口类
│ ├──model
│ │ └──WindowModel.ets // 应用后端数据管理类
│ ├──pages
│ │ ├──HomePage.ets // 登录之后的首页
│ │ ├──LoginPage.ets // 登录页面
│ │ ├──SuccessPage.ets // 验证码校验成功页面
│ │ └──VerifyPage.ets // 输入验证码页面
│ └──viewmodel
│ ├──GridItem.ets // 首页网格数据实体类
│ ├──WindowViewModel.ets // 应用界面数据管理类
│ └──VerifyItem.ets // 验证码数据实体类
└──entry/src/main/resources // 资源文件目录
4. 公共文件与资源
本案例涉及到的公共文件包括公共常量类、全局上下文类、日志类三个部分。
- 公共常量类(CommonConstants)
// main/ets/common/constants/CommonConstants.ets
export default class CommonConstants {
static readonly INPUT_ACCOUNT_LENGTH: number = 11
static readonly INPUT_PASSWORD_LENGTH: number = 8
static readonly COMMON_SPACE: number = 12
static readonly LOGIN_WAIT_TIME: number = 2000
static readonly SUCCESS_PAGE_URL: string = 'pages/SuccessPage'
static readonly HOME_PAGE_URL: string = 'pages/HomePage'
static readonly LOGIN_PAGE_URL: string = 'pages/LoginPage'
static readonly VERIFY_PAGE_URL: string = 'pages/VerifyPage'
static readonly HOME_PAGE_ACTION: string = 'startHomePage'
static readonly SUB_WINDOW_NAME: string = 'subWindow'
static readonly STATUS_BAR_COLOR: string = '#F1F3F5'
static readonly STATUS_BAR_CONTENT_COLOR: string = '#000000'
static readonly SUB_WINDOW_WIDTH_RATIO: number = 0.93
static readonly SUB_WINDOW_ASPECT_RATIO: number = 1.25
static readonly FULL_PARENT: string = '100%'
static readonly BUTTON_WIDTH: string = '86.7%'
static readonly GRID_FOUR_COLUMNS: string = '1fr 1fr 1fr 1fr'
static readonly GRID_TWO_ROWS: string = '1fr 1fr'
static readonly GRID_TWO_COLUMNS: string = '1fr 1fr'
static readonly GRID_THREE_ROWS: string = '1fr 1fr 1fr'
}
2. 全局上下文单例模式类(GlobalContext)
// main/ets/common/utils/GlobalContext.ets
export class GlobalContext {
private constructor() {
}
private static instance: GlobalContext
private _objects = new Map<string, Object>()
public static getContext(): GlobalContext {
if (!GlobalContext.instance) {
GlobalContext.instance = new GlobalContext()
}
return GlobalContext.instance
}
getObject(value: string): Object | undefined {
return this._objects.get(value)
}
setObject(key: string, objectClass: Object): void {
this._objects.set(key, objectClass)
}
}
3. 日志类(Logger)
import { hilog } from '@kit.PerformanceAnalysisKit'
export class Logger {
private domain: number
private prefix: string
private format: string = '%{public}s, %{public}s'
constructor(prefix: string = 'WindowManager', domain: number = 0xFF00) {
this.prefix = prefix
this.domain = domain
}
debug(...args: string[]): void {
hilog.debug(this.domain, this.prefix, this.format, args)
}
info(...args: string[]): void {
hilog.info(this.domain, this.prefix, this.format, args)
}
warn(...args: string[]): void {
hilog.warn(this.domain, this.prefix, this.format, args)
}
error(...args: string[]): void {
hilog.error(this.domain, this.prefix, this.format, args)
}
}
export default new Logger()
本案例涉及到的资源文件如下:
- string.json
// main/resources/base/element/string.json
// main/resources/en_US/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "WindowManager"
},
{
"name": "login_page",
"value": "Login page"
},
{
"name": "login_more",
"value": "Log in to your account to use more services"
},
{
"name": "account",
"value": "Account"
},
{
"name": "password",
"value": "Password"
},
{
"name": "verify",
"value": "Login via Verification Code"
},
{
"name": "register_account",
"value": "Registering an Account"
},
{
"name": "forgot_password",
"value": "Forgot Password"
},
{
"name": "message_login",
"value": "Login via SMS"
},
{
"name": "change_one",
"value": "Change One"
},
{
"name": "verify_input_placeholder",
"value": "Please enter the verification code"
},
{
"name": "verify_wrong_hints",
"value": "Incorrect verification code. Please try again"
},
{
"name": "verify_hints",
"value": "To protect your network security, please enter the verification code"
},
{
"name": "verify_ok",
"value": "OK"
},
{
"name": "success",
"value": "Verification success"
},
{
"name": "success_login_hints",
"value": "The login page is automatically displayed in 2 seconds"
},
{
"name": "my_love",
"value": "My favorite"
},
{
"name": "history_record",
"value": "History"
},
{
"name": "message",
"value": "Message"
},
{
"name": "shopping_cart",
"value": "Shopping cart"
},
{
"name": "my_goal",
"value": "My goal"
},
{
"name": "group",
"value": "Group"
},
{
"name": "favorites",
"value": "Favorites"
},
{
"name": "recycle_bin",
"value": "Recycle Bin"
},
{
"name": "home_top",
"value": "Rankings"
},
{
"name": "home_text_top",
"value": "XiaMen Station, we'll see you."
},
{
"name": "home_new",
"value": "New product launch"
},
{
"name": "home_text_new",
"value": "XiaMen Station, we'll see you."
},
{
"name": "home_brand",
"value": "Big-name flash shopping"
},
{
"name": "home_text_brand",
"value": "More Big Names"
},
{
"name": "home_found",
"value": "Discover the good stuff"
},
{
"name": "home_text_found",
"value": "XiaMen Station, we'll see you."
},
{
"name": "home_list",
"value": "List"
},
{
"name": "home_title",
"value": "Home"
}
]
}
// main/resources/zh_CN/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "模块描述"
},
{
"name": "EntryAbility_desc",
"value": "描述"
},
{
"name": "EntryAbility_label",
"value": "窗口管理"
},
{
"name": "login_page",
"value": "登录界面"
},
{
"name": "login_more",
"value": "登录帐号以使用更多服务"
},
{
"name": "account",
"value": "帐号"
},
{
"name": "password",
"value": "密码"
},
{
"name": "verify",
"value": "验证码检测"
},
{
"name": "register_account",
"value": "注册帐号"
},
{
"name": "forgot_password",
"value": "忘记密码"
},
{
"name": "message_login",
"value": "短信验证码登录"
},
{
"name": "change_one",
"value": "换一张"
},
{
"name": "verify_input_placeholder",
"value": "请输入验证码"
},
{
"name": "verify_wrong_hints",
"value": "验证码错误,请重新输入"
},
{
"name": "verify_hints",
"value": "为了保护你的网络安全,请输入验证码"
},
{
"name": "verify_ok",
"value": "确定"
},
{
"name": "success",
"value": "验证成功"
},
{
"name": "success_login_hints",
"value": "2s后自动跳转登录"
},
{
"name": "my_love",
"value": "我的最爱"
},
{
"name": "history_record",
"value": "历史记录"
},
{
"name": "message",
"value": "消息"
},
{
"name": "shopping_cart",
"value": "购物车"
},
{
"name": "my_goal",
"value": "我的目标"
},
{
"name": "group",
"value": "圈子"
},
{
"name": "favorites",
"value": "收藏"
},
{
"name": "recycle_bin",
"value": "回收站"
},
{
"name": "home_top",
"value": "排行榜"
},
{
"name": "home_text_top",
"value": "厦门站,我们不见不散"
},
{
"name": "home_new",
"value": "新品首发"
},
{
"name": "home_text_new",
"value": "厦门站,我们不见不散"
},
{
"name": "home_brand",
"value": "大牌闪购"
},
{
"name": "home_text_brand",
"value": "更多大牌"
},
{
"name": "home_found",
"value": "发现好物"
},
{
"name": "home_text_found",
"value": "厦门站,我们不见不散"
},
{
"name": "home_list",
"value": "列表"
},
{
"name": "home_title",
"value": "首页"
}
]
}
2. float.json
// main/resources/base/element/float.json
{
"float": [
{
"name": "logo_image_size",
"value": "78vp"
},
{
"name": "logo_margin_top",
"value": "125vp"
},
{
"name": "logo_margin_bottom",
"value": "9vp"
},
{
"name": "page_title_text_size",
"value": "24fp"
},
{
"name": "normal_text_size",
"value": "16fp"
},
{
"name": "large_text_size",
"value": "21fp"
},
{
"name": "big_text_size",
"value": "16fp"
},
{
"name": "small_text_size",
"value": "14fp"
},
{
"name": "little_text_size",
"value": "12fp"
},
{
"name": "login_more_margin_bottom",
"value": "24vp"
},
{
"name": "login_more_margin_top",
"value": "8vp"
},
{
"name": "login_input_height",
"value": "48vp"
},
{
"name": "forgot_margin_top",
"value": "12vp"
},
{
"name": "forgot_margin",
"value": "12vp"
},
{
"name": "input_padding",
"value": "12vp"
},
{
"name": "line_height",
"value": "0.5vp"
},
{
"name": "line_margin",
"value": "12vp"
},
{
"name": "login_button_height",
"value": "40vp"
},
{
"name": "login_button_margin_top",
"value": "231vp"
},
{
"name": "login_register_margin_bottom",
"value": "40vp"
},
{
"name": "login_button_margin_bottom",
"value": "12vp"
},
{
"name": "login_page_padding_bottom",
"value": "24vp"
},
{
"name": "login_background_opacity",
"value": "0.2"
},
{
"name": "login_padding",
"value": "12vp"
},
{
"name": "verify_padding",
"value": "24vp"
},
{
"name": "verify_image_height",
"value": "48vp"
},
{
"name": "verify_image_width",
"value": "164vp"
},
{
"name": "verify_hints_width",
"value": "88vp"
},
{
"name": "verify_hints_padding",
"value": "40vp"
},
{
"name": "verify_text_input_margin",
"value": "8vp"
},
{
"name": "verify_ok_margin",
"value": "32vp"
},
{
"name": "verify_bottom_padding",
"value": "12vp"
},
{
"name": "verify_auto_jump_bottom_margin",
"value": "24vp"
},
{
"name": "success_image_size",
"value": "56vp"
},
{
"name": "success_image_margin",
"value": "32vp"
},
{
"name": "home_padding",
"value": "12vp"
},
{
"name": "home_tab_titles_margin",
"value": "12vp"
},
{
"name": "home_tab_titles_padding",
"value": "12vp"
},
{
"name": "home_swiper_border_radius",
"value": "16vp"
},
{
"name": "home_swiper_margin",
"value": "24vp"
},
{
"name": "home_grid_columns_gap",
"value": "8vp"
},
{
"name": "home_grid_row_gap",
"value": "12vp"
},
{
"name": "home_grid_padding",
"value": "12vp"
},
{
"name": "home_grid_height",
"value": "124vp"
},
{
"name": "background_text_margin",
"value": "4vp"
},
{
"name": "home_list_padding",
"value": "8vp"
},
{
"name": "background_border_radius",
"value": "24vp"
},
{
"name": "home_text_margin",
"value": "12vp"
},
{
"name": "home_background_border_radius",
"value": "12vp"
},
{
"name": "home_second_grid_height",
"value": "406vp"
},
{
"name": "home_home_cell_size",
"value": "24vp"
},
{
"name": "home_home_cell_margin",
"value": "4vp"
},
{
"name": "home_button_bottom",
"value": "55vp"
}
]
}
3. color.json
// main/resources/base/element/color.json
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "background",
"value": "#F1F3F5"
},
{
"name": "title_text_color",
"value": "#182431"
},
{
"name": "login_more_text_color",
"value": "#99182431"
},
{
"name": "placeholder_color",
"value": "#99182431"
},
{
"name": "line_color",
"value": "#0D000000"
},
{
"name": "login_button_color",
"value": "#007DFF"
},
{
"name": "login_button_disable",
"value": "#66007DFF"
},
{
"name": "login_font_disable",
"value": "#FFFFFF"
},
{
"name": "login_blue_text_color",
"value": "#007DFF"
},
{
"name": "verify_wrong_hints_color",
"value": "#FFFA2A2D"
},
{
"name": "home_grid_font_color",
"value": "#99182431"
}
]
}
其他资源请到源码中获取。
5. 页面沉浸式设置
5.1. EntryAbility
// main/ets/entryability/EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import CommonConstants from '../common/constants/CommonConstants'
import Logger from '../common/utils/Logger'
import WindowModel from '../model/WindowModel'
// 定义EntryAbility类,继承自UIAbility
export default class EntryAbility extends UIAbility {
// 当窗口阶段创建时调用
onWindowStageCreate(windowStage: window.WindowStage) {
Logger.info('Ability onWindowStageCreate')
// 获取WindowModel的单例实例
let windowModel = WindowModel.getInstance()
// 设置当前窗口阶段
windowModel.setWindowStage(windowStage)
// 设置主窗口为沉浸式(全屏模式)
windowModel.setMainWindowImmersive()
// 加载指定页面(LoginPage)的内容
windowStage.loadContent(CommonConstants.LOGIN_PAGE_URL, (err) => {
if (err.code) {
Logger.error(`Failed to load the content. Code:${err.code},message:${err.message}`)
return
}
})
}
}
关键代码说明:
- 在EntryAbility里的onWindowStageCreate生命周期函数里,获取WindowModel的单例实例。
- 设置当前窗口阶段。
- 设置主窗口为沉浸式(全屏模式)。
- 加载LoginPage页面。
5.2. WindowModel
// main/ets/model/WindowModel.ets
import { window, display } from '@kit.ArkUI' // 导入ArkUI的window和display模块,用于窗口管理和屏幕信息获取
import CommonConstants from '../common/constants/CommonConstants' // 导入公共常量
import Logger from '../common/utils/Logger' // 导入日志工具
import { GlobalContext } from '../common/utils/GlobalContext' // 导入全局上下文工具
import { BusinessError } from '@kit.BasicServicesKit' // 导入业务错误类型
// 定义WindowModel类,用于管理窗口的创建、配置和销毁
export default class WindowModel {
private windowStage?: window.WindowStage // 当前窗口阶段实例
private subWindowClass?: window.Window // 子窗口实例
// 获取WindowModel的单例实例
static getInstance(): WindowModel {
let instance: WindowModel = GlobalContext.getContext().getObject('windowModel') as WindowModel
if (instance === undefined) { // 如果实例不存在,则创建一个新的实例
instance = new WindowModel()
GlobalContext.getContext().setObject('windowModel', instance) // 将实例存储到全局上下文中
}
return instance
}
// 设置当前窗口阶段
setWindowStage(windowStage: window.WindowStage) {
this.windowStage = windowStage
}
// 创建子窗口
// ...
// 设置主窗口为沉浸式模式
setMainWindowImmersive() {
if (this.windowStage === undefined) { // 如果窗口阶段未定义,记录错误日志并返回
Logger.error('windowStage is undefined.')
return
}
// 获取主窗口实例
this.windowStage.getMainWindow((err, windowClass: window.Window) => {
if (err.code) { // 如果获取失败,记录错误日志并返回
Logger.error(`Failed to obtain the main window. Code:${err.code}, message:${err.message}`)
return
}
let isLayoutFullScreen = true
try {
// 设置窗口布局为全屏模式
let promise = windowClass.setWindowLayoutFullScreen(isLayoutFullScreen)
promise.then(() => {
Logger.info('Succeeded in setting the window layout to full-screen mode.')
}).catch((err: BusinessError) => { // 如果设置失败,记录错误日志
Logger.error(`Failed to set the window layout to full-screen mode. Cause code: ${err.code}, message: ${err.message}`)
})
} catch (exception) { // 如果捕获到异常,记录错误日志
Logger.error(`Failed to set the window layout to full-screen mode. Cause code: ${exception.code}, message: ${exception.message}`)
}
let names: Array<'status' | 'navigation'> = []
try {
// 设置系统栏(状态栏和导航栏)为不可见
let promise = windowClass.setWindowSystemBarEnable(names)
promise.then(() => {
Logger.info('Succeeded in setting the system bar to be invisible.')
}).catch((err: BusinessError) => { // 如果设置失败,记录错误日志
Logger.error(`Failed to set the system bar to be invisible. Cause code: ${err.code}, message: ${err.message}`)
})
} catch (exception) { // 如果捕获到异常,记录错误日志
Logger.error(`Failed to set the system bar to be invisible. Cause code: ${exception.code}, message: ${exception.message}`)
}
})
}
// 销毁子窗口
// ...
}
关键代码说明:
- 本部分代码只实现了EntryAbility需要的功能。
- 实现了定义WindowModel类。
- 实现了获取WindowModel的单例实例的getInstance方法。
- 实现了设置当前窗口阶段setWindowStage方法。
- 实现了设置主窗口为沉浸式模式setMainWindowImmersive方法。
6. 登录页面
6.1. LoginPage
// main/ets/pages/LoginPage.ets
import router from '@ohos.router'
import CommonConstants from '../common/constants/CommonConstants'
import Logger from '../common/utils/Logger'
import WindowModel from '../model/WindowModel'
// 定义一个扩展函数,用于设置TextInput的样式
@Extend(TextInput)
function inputStyle() {
.placeholderColor($r('app.color.placeholder_color'))
.backgroundColor($r('app.color.start_window_background'))
.height($r('app.float.login_input_height'))
.fontSize($r('app.float.big_text_size'))
.padding({
left: $r('app.float.input_padding'),
right: $r('app.float.input_padding')
})
}
// 定义一个扩展函数,用于设置Text的样式
@Extend(Text)
function blueTextStyle() {
.fontColor($r('app.color.login_blue_text_color'))
.fontSize($r('app.float.small_text_size'))
.fontWeight(FontWeight.Medium)
.margin({
left: $r('app.float.forgot_margin'),
right: $r('app.float.forgot_margin')
})
}
// 定义登录页面组件
@Entry
@ComponentV2
struct LoginPage {
@Local account: string = '' // 本地状态,存储账号
@Local password: string = '' // 本地状态,存储密码
@Local isShadow: boolean = false // 本地状态,控制阴影显示
private windowModel: WindowModel = WindowModel.getInstance() // 获取窗口模型实例
@Local isHome: boolean = false // 本地状态,控制是否跳转到主页
// 生命周期函数,页面即将显示时调用
aboutToAppear() {
// 监听HOME_PAGE_ACTION事件,当事件触发时设置isHome为true
getContext(this).eventHub.on(CommonConstants.HOME_PAGE_ACTION, () => {
this.isHome = true
})
}
// 构建UI
build() {
Stack({ alignContent: Alignment.Top }) {
Column() {
Image($r('app.media.ic_logo'))
.width($r('app.float.logo_image_size'))
.height($r('app.float.logo_image_size'))
.margin({
top: $r('app.float.logo_margin_top'),
bottom: $r('app.float.logo_margin_bottom')
})
Text($r('app.string.login_page'))
.fontSize($r('app.float.page_title_text_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.title_text_color'))
Text($r('app.string.login_more'))
.fontSize($r('app.float.normal_text_size'))
.fontColor($r('app.color.login_more_text_color'))
.margin({
bottom: $r('app.float.login_more_margin_bottom'),
top: $r('app.float.login_more_margin_top')
})
Column() {
TextInput({ placeholder: $r('app.string.account') }) // 账号输入框
.maxLength(CommonConstants.INPUT_ACCOUNT_LENGTH)
.inputStyle()
.onChange((value: string) => { // 监听输入变化
this.account = value // 更新账号状态
})
Line() // 显示分割线
.width(CommonConstants.FULL_PARENT)
.height($r('app.float.line_height'))
.backgroundColor($r('app.color.line_color'))
.margin({
left: $r('app.float.line_margin'),
right: $r('app.float.line_margin')
})
TextInput({ placeholder: $r('app.string.password') }) // 密码输入框
.maxLength(CommonConstants.INPUT_PASSWORD_LENGTH)
.type(InputType.Password)
.inputStyle()
.onChange((value: string) => { // 监听输入变化
this.password = value // 更新密码状态
})
}
.padding({
top: $r('app.float.background_text_margin'),
bottom: $r('app.float.background_text_margin')
})
.width(CommonConstants.FULL_PARENT)
.backgroundColor($r('app.color.start_window_background'))
.borderRadius($r('app.float.background_border_radius'))
Row() {
Text($r('app.string.message_login')).blueTextStyle()
Text($r('app.string.forgot_password')).blueTextStyle()
}
.justifyContent(FlexAlign.SpaceBetween)
.width(CommonConstants.FULL_PARENT)
.margin({ top: $r('app.float.forgot_margin_top') })
Blank() // 空白占位符
Button($r('app.string.verify'), { type: ButtonType.Capsule }) // 显示验证按钮
.width(CommonConstants.BUTTON_WIDTH)
.height($r('app.float.login_button_height'))
.fontSize($r('app.float.normal_text_size'))
.fontWeight(FontWeight.Medium)
.enabled(isLoginClickable(this.account, this.password))
.backgroundColor(isLoginClickable(this.account, this.password) ? $r('app.color.login_button_color') :
$r('app.color.login_button_disable'))
.fontColor(isLoginClickable(this.account, this.password) ? Color.White : $r('app.color.login_font_disable'))
.margin({
top: $r('app.float.login_button_margin_top'),
bottom: $r('app.float.login_button_margin_bottom')
})
.onClick(() => { // 监听按钮点击事件
this.windowModel.createSubWindow() // 创建子窗口
this.isShadow = true // 显示阴影
})
Text($r('app.string.register_account'))
.fontColor($r('app.color.login_blue_text_color'))
.fontSize($r('app.float.normal_text_size'))
.fontWeight(FontWeight.Medium)
.margin({ bottom: $r('app.float.login_register_margin_bottom') })
}
.backgroundColor($r('app.color.background'))
.height(CommonConstants.FULL_PARENT)
.width(CommonConstants.FULL_PARENT)
.padding({
left: $r('app.float.login_padding'),
right: $r('app.float.login_padding'),
bottom: $r('app.float.login_page_padding_bottom')
})
if (this.isShadow) { // 如果isShadow为true,显示阴影
Rect()
.width(CommonConstants.FULL_PARENT)
.height(CommonConstants.FULL_PARENT)
.fillOpacity($r('app.float.login_background_opacity'))
.fill($r('app.color.title_text_color'))
}
}
}
// 监听isHome状态的变化
@Monitor('isHome')
toHome(monitor: IMonitor) {
if (this.isHome) { // 如果isHome为true,跳转到主页
router.replaceUrl({
url: CommonConstants.HOME_PAGE_URL
}).catch((err: Error) => {
Logger.error(`pushUrl failed, message:${err.message}`)
})
}
}
}
// 判断登录按钮是否可点击
function isLoginClickable(account: string, password: string): boolean {
return account !== '' && password !== '' // 当账号和密码都不为空时返回true
}
关键代码说明:
- 生命周期函数aboutToAppear,注册监听了HOME_PAGE_ACTION事件,当事件触发时设置isHome为true。
- this.windowModel.createSubWindow(),创建了子窗口,用于显示验证页面和验证成功页面。
- Rect()在isShadow为true时,显示一个模态窗口的蒙版。
- @Monitor('isHome'),用于监听isHome状态的变化,如果isHome为true,则跳转到HomePage页面。
6.2. EntryAbility
import { window, display } from '@kit.ArkUI'
import CommonConstants from '../common/constants/CommonConstants'
import Logger from '../common/utils/Logger'
import { GlobalContext } from '../common/utils/GlobalContext'
import { BusinessError } from '@kit.BasicServicesKit'
// 定义WindowModel类,用于管理窗口的创建、配置和销毁
export default class WindowModel {
private windowStage?: window.WindowStage // 当前窗口阶段实例
private subWindowClass?: window.Window // 子窗口实例
// 获取WindowModel的单例实例
// ...
// 设置当前窗口阶段
// ...
// 创建子窗口
createSubWindow() {
if (this.windowStage === undefined) { // 如果窗口阶段未定义,记录错误日志并返回
Logger.error('Failed to create the subWindow.')
return
}
// 创建子窗口
this.windowStage.createSubWindow(CommonConstants.SUB_WINDOW_NAME, (err, data: window.Window) => {
if (err.code) { // 如果创建失败,记录错误日志并返回
Logger.error(`Failed to create the window. Code:${err.code}, message:${err.message}`)
return
}
// 获取子窗口实例
this.subWindowClass = data
// 获取屏幕的宽度和高度
let screenWidth = display.getDefaultDisplaySync().width
let screenHeight = display.getDefaultDisplaySync().height
// 根据子窗口的宽高比计算子窗口的宽度和高度
let windowWidth = screenWidth * CommonConstants.SUB_WINDOW_WIDTH_RATIO
let windowHeight = windowWidth / CommonConstants.SUB_WINDOW_ASPECT_RATIO
// 计算子窗口的起始坐标
let moveX = (screenWidth - windowWidth) / 2
let moveY = screenHeight - windowHeight
// 移动子窗口到指定位置
this.subWindowClass.moveWindowTo(moveX, moveY, (err) => {
if (err.code) { // 如果移动失败,记录错误日志并返回
Logger.error(`Failed to move the window. Code:${err.code}, message:${err.message}`)
return
}
})
// 调整子窗口的大小
this.subWindowClass.resize(windowWidth, windowHeight, (err) => {
if (err.code) { // 如果调整大小失败,记录错误日志并返回
Logger.error(`Failed to change the window size. Code:${err.code}, message:${err.message}`)
return
}
})
// 设置子窗口的内容
this.subWindowClass.setUIContent(CommonConstants.VERIFY_PAGE_URL, (err) => {
if (err.code) { // 如果设置内容失败,记录错误日志并返回
Logger.error(`Failed to load the content. Code:${err.code}, message:${err.message}`)
return
}
if (this.subWindowClass === undefined) { // 如果子窗口实例未定义,记录错误日志并返回
Logger.error('subWindowClass is undefined.')
return
}
// 设置子窗口的背景颜色为透明
this.subWindowClass.setWindowBackgroundColor('#00000000')
// 显示子窗口
this.subWindowClass.showWindow((err) => {
if (err.code) { // 如果显示失败,记录错误日志并返回
Logger.error(`Failed to show the window. Code:${err.code}, essage:${err.message}`)
return
}
})
})
})
}
// 设置主窗口为沉浸式模式
// ...
// 销毁子窗口
// ...
}
关键代码说明:
- createSubWindow方法实现了创建子窗口。
- setUIContent设置了子窗口的内容为VerifyPage页面。
7. 校验子窗口页面
7.1. 校验页面
// main/ets/pages/VerifyPage.ets
import { router } from '@kit.ArkUI'
import CommonConstants from '../common/constants/CommonConstants'
import Logger from '../common/utils/Logger'
import VerifyItem from '../viewmodel/VerifyItem' // 导入验证项数据模型
import WindowViewModel from '../viewmodel/WindowViewModel' // 导入窗口视图模型
// 定义一个扩展函数,用于设置Text的样式
@Extend(Text)
function promptTextStyle() {
.fontSize($r('app.float.small_text_size'))
.width(CommonConstants.FULL_PARENT)
.padding({
left: $r('app.float.verify_padding'),
right: $r('app.float.verify_padding')
})
}
// 定义验证页面组件
@Entry
@ComponentV2
struct VerifyPage {
@Local isInputWrong: boolean = false
@Local verifyItem: VerifyItem = new VerifyItem($r('app.media.ic_verity_character1'), 'XYZK')
@Local inputText: string = ''
private verifyMap: Map<number, VerifyItem> = new Map()
private imageId: number = 0
// 生命周期函数,页面即将显示时调用
aboutToAppear() {
// 从WindowViewModel获取验证项映射表
this.verifyMap = WindowViewModel.getVerifyMap()
// 更新当前验证项
this.updateVerifyItem()
}
// 更新当前验证项
updateVerifyItem(){
// 根据imageId获取新的验证项
let verifyItemNew: VerifyItem | undefined = this.verifyMap.get(this.imageId)
if (verifyItemNew !== undefined) {
// 更新当前验证项
this.verifyItem = verifyItemNew
}
}
build() {
Column() {
Column() {
Row() {
Image(this.verifyItem.image) // 显示验证图片
.height($r('app.float.verify_image_height'))
.width($r('app.float.verify_image_width'))
Text($r('app.string.change_one'))
.fontColor($r('app.color.login_blue_text_color'))
.fontSize($r('app.float.big_text_size'))
.width($r('app.float.verify_hints_width'))
.textAlign(TextAlign.Center)
.onClick(() => { // 监听点击事件
this.imageId = (this.imageId + 1) % 2 // 切换验证项ID
this.updateVerifyItem() // 更新验证项
})
}
.padding({
left: $r('app.float.verify_padding'),
top: $r('app.float.verify_padding'),
right: $r('app.float.verify_hints_padding')
})
.width(CommonConstants.FULL_PARENT)
.justifyContent(FlexAlign.SpaceBetween)
// 显示输入框
TextInput({ placeholder: $r('app.string.verify_input_placeholder'), text: this.inputText })
.placeholderColor($r('app.color.placeholder_color'))
.fontSize($r('app.float.big_text_size'))
.height($r('app.float.login_button_height'))
.margin({
left: $r('app.float.verify_padding'),
right: $r('app.float.verify_padding'),
top: $r('app.float.verify_text_input_margin'),
bottom: $r('app.float.verify_text_input_margin')
})
.onChange((value: string) => {
this.inputText = value
})
if (this.isInputWrong) { // 如果输入错误
Text($r('app.string.verify_wrong_hints'))
.fontColor($r('app.color.verify_wrong_hints_color'))
.promptTextStyle()
} else { // 如果输入正确
Text($r('app.string.verify_hints'))
.fontColor($r('app.color.placeholder_color'))
.promptTextStyle()
}
Text($r('app.string.verify_ok')) // 显示“确定”按钮
.fontSize($r('app.float.big_text_size'))
.fontColor($r('app.color.login_blue_text_color'))
.margin({
top: $r('app.float.verify_ok_margin'),
bottom: $r('app.float.verify_ok_margin')
})
.onClick(() => { // 监听点击事件
let verifyText: string = this.verifyItem.characters
// 获取验证项的正确字符
if (this.inputText.toLowerCase() === verifyText.toLowerCase()) { // 如果输入正确
router.replaceUrl({ // 跳转到成功页面
url: CommonConstants.SUCCESS_PAGE_URL
}).catch((err: Error) => { // 如果跳转失败,记录错误日志
Logger.error(`pushUrl failed, message:${err.message}`)
})
} else { // 如果输入错误
this.isInputWrong = true // 设置输入错误状态为true
this.inputText = '' // 清空输入内容
this.imageId = (this.imageId + 1) % 2 // 切换验证项ID
this.updateVerifyItem() // 更新验证项
}
})
}
.backgroundColor(Color.White)
.height(CommonConstants.FULL_PARENT)
.borderRadius($r('app.float.verify_ok_margin'))
}
.padding({ bottom: $r('app.float.verify_bottom_padding') })
.height(CommonConstants.FULL_PARENT)
}
}
关键代码说明:
- verifyMap: Map<number, VerifyItem>,定义验证Map。
- 在aboutToAppear里从WindowViewModel获取验证项映射表WindowViewModel.getVerifyMap()。
- updateVerifyItem函数更新验证,
- this.inputText.toLowerCase() === verifyText.toLowerCase() 获取验证项的正确字符,跳转到成功页面SuccessPage。
7.2. 验证项数据模型
export default class VerifyItem {
image: Resource
characters: string
constructor(image: Resource, characters: string) {
this.image = image
this.characters = characters
}
}
7.3. 窗口视图模型
// main/ets/viewmodel/WindowViewModel.ets
// ...
import VerifyItem from './VerifyItem'
export class WindowViewModel {
getVerifyMap(): Map<number, VerifyItem> {
let verifyMap: Map<number, VerifyItem> = new Map()
verifyMap.set(0, new VerifyItem($r('app.media.ic_verity_character1'), 'XYZK'))
verifyMap.set(1, new VerifyItem($r('app.media.ic_verity_character2'), 'LHBR'))
return verifyMap
}
// ...
}
export default new WindowViewModel()
8. 校验成功页面
// main/ets/pages/SuccessPage.ets
import CommonConstants from '../common/constants/CommonConstants'
import WindowModel from '../model/WindowModel'
@Entry
@ComponentV2
struct SuccessPage {
// 生命周期函数,页面即将显示时调用
aboutToAppear() {
// 设置一个定时器,延迟执行以下操作
setTimeout(() => {
// 销毁子窗口
WindowModel.getInstance().destroySubWindow()
// 触发HOME_PAGE_ACTION事件
getContext(this).eventHub.emit(CommonConstants.HOME_PAGE_ACTION)
}, CommonConstants.LOGIN_WAIT_TIME)
}
build() {
Column() {
Column() {
Text($r('app.string.success')) // 显示“成功”文本
.fontSize($r('app.float.large_text_size'))
.padding({
left: $r('app.float.verify_padding'),
top: $r('app.float.verify_padding')
})
.width(CommonConstants.FULL_PARENT)
Image($r('app.media.ic_success')) // 显示成功图片
.width($r('app.float.success_image_size'))
.height($r('app.float.success_image_size'))
.margin({
top: $r('app.float.success_image_margin'),
bottom: $r('app.float.success_image_margin')
})
Text($r('app.string.success_login_hints')) // 显示成功登录提示
.fontSize($r('app.float.small_text_size'))
.fontColor($r('app.color.placeholder_color'))
.margin({
bottom: $r('app.float.verify_auto_jump_bottom_margin')
})
}
.backgroundColor(Color.White)
.height(CommonConstants.FULL_PARENT)
.borderRadius($r('app.float.verify_ok_margin'))
}
.padding({ bottom: $r('app.float.verify_bottom_padding') })
.height(CommonConstants.FULL_PARENT)
}
}
关键代码说明:
- 在aboutToAppear里,两秒后自动销毁子窗口并触发打开首页面(HomePage)的事件。
- WindowModel.getInstance().destroySubWindow() 销毁子窗口。
- getContext(this).eventHub.emit(CommonConstants.HOME_PAGE_ACTION),触发HOME_PAGE_ACTION事件。
9. 首页面
9.1. HomePage
// main/ets/pages/HomePage.ets
import CommonConstants from '../common/constants/CommonConstants'
import ItemData from '../viewmodel/GridItem'
import WindowViewModel from '../viewmodel/WindowViewModel'
@Entry
@ComponentV2
struct HomePage {
private swiperController: SwiperController = new SwiperController()
// 定义导航栏标题的UI构建函数
@Builder
NavigationTitle() {
Text($r('app.string.home_title'))
.fontWeight(FontWeight.Medium)
.fontSize($r('app.float.page_title_text_size'))
.margin({ top: $r('app.float.home_tab_titles_margin') })
.padding({ left: $r('app.float.home_tab_titles_padding') })
}
build() {
Navigation() { // 使用Navigation组件作为页面容器
Scroll() { // 使用Scroll组件实现页面滚动
Column({ space: CommonConstants.COMMON_SPACE }) {
// 轮播图部分
Swiper(this.swiperController) {
ForEach(WindowViewModel.getSwiperImages(), (img: Resource, index?: number) => {
Image(img).borderRadius($r('app.float.home_swiper_border_radius'))
}, (img: Resource, index?: number) => index + JSON.stringify(img.id))
}
.margin({
top: $r('app.float.home_swiper_margin')
})
.autoPlay(true)
// 第一个Grid部分
Grid() {
ForEach(WindowViewModel.getFirstGridData(), (firstItem: ItemData, index?: number) => {
GridItem() {
Column() {
Image(firstItem.image)
.width($r('app.float.home_home_cell_size'))
.height($r('app.float.home_home_cell_size'))
Text(firstItem.title)
.fontSize($r('app.float.little_text_size'))
.margin({ top: $r('app.float.home_home_cell_margin') })
}
}
}, (firstItem: ItemData, index?: number) => index + JSON.stringify(firstItem))
}
.columnsTemplate(CommonConstants.GRID_FOUR_COLUMNS)
.rowsTemplate(CommonConstants.GRID_TWO_ROWS)
.columnsGap($r('app.float.home_grid_columns_gap'))
.rowsGap($r('app.float.home_grid_row_gap'))
.padding({
top: $r('app.float.home_grid_padding'),
bottom: $r('app.float.home_grid_padding')
})
.height($r('app.float.home_grid_height'))
.backgroundColor(Color.White)
.borderRadius($r('app.float.background_border_radius'))
// 列表标题
Text($r('app.string.home_list'))
.fontSize($r('app.float.normal_text_size'))
.fontWeight(FontWeight.Medium)
.width(CommonConstants.FULL_PARENT)
.margin({ top: $r('app.float.home_text_margin') })
// 第二个Grid部分
Grid() {
ForEach(WindowViewModel.getSecondGridData(), (secondItem: ItemData, index?: number) => {
GridItem() {
Column() {
Text(secondItem.title) // 显示标题
.fontSize($r('app.float.normal_text_size'))
.fontWeight(FontWeight.Medium)
Text(secondItem.others)
.margin({ top: $r('app.float.background_text_margin') })
.fontSize($r('app.float.little_text_size'))
.fontColor($r('app.color.home_grid_font_color'))
}
.alignItems(HorizontalAlign.Start)
}
.padding({
top: $r('app.float.home_list_padding'),
left: $r('app.float.home_list_padding')
})
.borderRadius($r('app.float.home_background_border_radius'))
.align(Alignment.TopStart)
.backgroundImage(secondItem.image)
.backgroundImageSize(ImageSize.Cover)
.width(CommonConstants.FULL_PARENT)
.height(CommonConstants.FULL_PARENT)
}, (secondItem: ItemData, index?: number) => index + JSON.stringify(secondItem))
}
.width(CommonConstants.FULL_PARENT)
.height($r('app.float.home_second_grid_height'))
.columnsTemplate(CommonConstants.GRID_TWO_COLUMNS)
.rowsTemplate(CommonConstants.GRID_THREE_ROWS)
.columnsGap($r('app.float.home_grid_columns_gap'))
.rowsGap($r('app.float.home_grid_row_gap'))
.margin({ bottom: $r('app.float.home_button_bottom') })
}
}
.scrollBar(BarState.Off)
.margin({
left: $r('app.float.home_padding'),
right: $r('app.float.home_padding')
})
}
.title(this.NavigationTitle()) // 设置导航栏标题
.titleMode(NavigationTitleMode.Mini) // 设置导航栏标题模式为Mini
.hideBackButton(true) // 隐藏返回按钮
.backgroundColor($r('app.color.background')) // 设置背景颜色
.mode(NavigationMode.Stack) // 设置导航模式为Stack
}
}
9.2. 构建首页面的VM
// main/ets/viewmodel/WindowViewModel.ets
import GridItem from './GridItem'
// ...
export class WindowViewModel {
// ...
getSwiperImages(): Array<Resource> {
let swiperImages: Resource[] = [
$r('app.media.ic_swiper1'),
$r('app.media.ic_swiper2'),
$r('app.media.ic_swiper3'),
$r('app.media.ic_swiper4')
]
return swiperImages
}
getFirstGridData(): Array<GridItem> {
let firstGridData: GridItem[] = [
new GridItem($r('app.string.my_love'), $r('app.media.ic_love')),
new GridItem($r('app.string.history_record'), $r('app.media.ic_record')),
new GridItem($r('app.string.message'), $r('app.media.ic_message')),
new GridItem($r('app.string.shopping_cart'), $r('app.media.ic_shopping')),
new GridItem($r('app.string.my_goal'), $r('app.media.ic_target')),
new GridItem($r('app.string.group'), $r('app.media.ic_circle')),
new GridItem($r('app.string.favorites'), $r('app.media.ic_favorite')),
new GridItem($r('app.string.recycle_bin'), $r('app.media.ic_recycle'))
]
return firstGridData
}
getSecondGridData(): Array<GridItem> {
let secondGridData: GridItem[] = [
new GridItem($r('app.string.home_top'), $r('app.media.ic_top'), $r('app.string.home_text_top')),
new GridItem($r('app.string.home_new'), $r('app.media.ic_new'), $r('app.string.home_text_new')),
new GridItem($r('app.string.home_brand'), $r('app.media.ic_brand'), $r('app.string.home_text_brand')),
new GridItem($r('app.string.home_found'), $r('app.media.ic_found'), $r('app.string.home_text_found')),
new GridItem($r('app.string.home_top'), $r('app.media.ic_top'), $r('app.string.home_text_top')),
new GridItem($r('app.string.home_new'), $r('app.media.ic_new'), $r('app.string.home_text_new'))
]
return secondGridData
}
}
export default new WindowViewModel()
✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接