时间:2026 年 5 月
框架:HarmonyOS ArkTS (Stage Model)
SDK:HarmonyOS 6.0 (API 23)


在这里插入图片描述
在这里插入图片描述

一、引言

2025 年的今天,鸿蒙生态已经日趋成熟。作为一名前端 / 移动端开发者,掌握 ArkTS(Ark TypeScript)——鸿蒙原生开发语言——已经成为必备技能之一。

本文将带您完整体验 从项目创建到登录页面完成 的全流程。我们不仅会实现一个功能完整的登录窗口(包含输入校验、按钮交互、页面跳转),还会深度剖析 ArkTS 的声明式 UI 语法、状态管理机制、组件化设计思想,以及如何通过主题配色传递产品情感——我们将使用 「秋季思雨」 风格,让登录页在功能之外也有温度。

无论您是初次接触鸿蒙开发,还是有一定经验想深入了解 ArkTS 的细节,这篇文章都值得一读。全文约 10000 字,代码完整可运行。


二、项目初始化与环境准备

2.1 开发环境

开始之前,请确保您已安装以下工具:

工具 版本 说明
DevEco Studio 6.0+ 鸿蒙官方 IDE,基于 IntelliJ
HarmonyOS SDK API 23 (6.1.1.24) 目标 SDK 版本
Node.js 18+ 用于 OHPM 包管理
ohpm 最新 鸿蒙包管理器

本文项目使用 Stage Model(阶段模型),这是鸿蒙推荐的应用开发模型,API Type 为 stageMode

2.2 创建项目

在 DevEco Studio 中点击 File → New → Create Project,选择:

  • 模板:Empty Ability
  • Project Type:Application
  • Compile SDK:6.1.1(24)
  • Model:Stage
  • Language:ArkTS

项目创建完成后,目录结构如下(已移除与本主题无关的文件):

Demo0528/
├── AppScope/
│   ├── app.json5                    # 应用级配置(bundleName、版本等)
│   └── resources/
│       ├── base/element/string.json
│       └── base/media/layered_image.json
├── entry/
│   └── src/main/
│       ├── ets/
│       │   ├── entryability/EntryAbility.ets   # Ability 入口
│       │   ├── entrybackupability/EntryBackupAbility.ets
│       │   └── pages/
│       │       ├── Index.ets                   # 首页(入口页面)
│       │       └── LoginPage.ets               # 登录页面(本文核心)
│       ├── module.json5                        # 模块配置
│       └── resources/
│           ├── base/element/
│           │   ├── color.json
│           │   ├── float.json
│           │   └── string.json
│           ├── base/profile/
│           │   └── main_pages.json             # 页面路由配置
│           └── dark/element/color.json
├── build-profile.json5                         # 应用级构建配置
├── hvigor/
└── oh-package.json5

2.3 配置文件解读

build-profile.json5 — 应用构建配置
{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "targetSdkVersion": "6.1.1(24)",
        "compatibleSdkVersion": "6.1.1(24)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        }
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [ { "name": "default", "applyToProducts": ["default"] } ]
    }
  ]
}

关键点说明:

  • compatibleSdkVersion 定义了最低兼容版本,targetSdkVersion 是目标版本。
  • strictMode.caseSensitiveCheck 开启大小写敏感检查,有助于团队规范。
  • 这里没有配置签名,调试运行使用 DevEco Studio 的自动签名即可。
AppScope/app.json5 — 应用元信息
{
  "app": {
    "bundleName": "com.shili.Demo0528",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:layered_image",
    "label": "$string:app_name"
  }
}
  • bundleName 是应用的唯一标识,类似 Android 的 package name,需要保证全局唯一。
  • $media:layered_image 引用资源文件夹中的图片资源。
module.json5 — 模块配置
{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone"],
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["ohos.want.action.home"]
          }
        ]
      }
    ]
  }
}

这里定义了应用有一个 EntryAbility,它作为启动入口(home 页面)。页面路由由 $profile:main_pages 引用外部 JSON 文件管理。

main_pages.json — 页面注册
{
  "src": [
    "pages/Index",
    "pages/LoginPage"
  ]
}

这是鸿蒙 Stage Model 的页面路由注册表。每一个新页面都必须在 src 数组中声明,才能被 router.pushUrl() 跳转。路径相对于 ets/ 目录,不包含 .ets 后缀。

注意:如果不添加 "pages/LoginPage",运行时跳转 LoginPage 会报错 page not found。这是初学者最容易踩的坑之一。


三、ArkTS 核心概念速览

在写代码之前,了解 ArkTS 的核心理念会帮助您更快地上手。

3.1 声明式 UI

ArkTS 采用 声明式 + 响应式 的 UI 范式。您只需描述 UI 应该是什么样子,框架自动处理 UI 更新。这与 React / SwiftUI / Jetpack Compose 是同一类思想。

// 命令式(传统方式)
text.setText('Hello');
text.setColor('red');

// 声明式(ArkTS 方式)
Text('Hello')
  .fontColor('red')

3.2 @Component 与 build()

每一个页面或组件都是一个 @Component 装饰的结构体:

@Component
struct MyComponent {
  build() {
    // 在这里声明 UI 树
  }
}
  • build() 是组件的渲染入口,必须返回一个容器或组件。
  • 根容器通常是 Column(纵向)或 Row(横向)。

3.3 @State 与响应式

@State username: string = '';
  • @State 装饰的变量是响应式数据源
  • @State 变量变化时,框架自动重新渲染依赖它的 UI 部分。
  • 不需要手动调用 setState()update()

3.4 链式调用

ArkTS 组件使用链式方法来配置属性,每个方法返回组件自身,可以连续调用:

Text('欢迎登录')
  .fontSize(28)
  .fontWeight(FontWeight.Bold)
  .fontColor('#3D2B1F')

这种风格写起来简洁直观,但注意方法调用的顺序——某些属性可能相互影响(例如先设置颜色再设置字体,颜色不会丢失,因为都是独立设置的)。

3.5 条件渲染

if (this.loginMessage.length > 0) {
  Text(this.loginMessage)
    .fontColor('#C94A4A')
}

ArkTS 支持 if/else 条件渲染。条件表达式中的变量必须是 @State@Prop 等响应式数据,否则条件判断一次后不再更新。


四、构建首页 Index.ets

首页的作用是作为应用入口,提供一个跳转到登录页的按钮。完整的代码如下:

// entry/src/main/ets/pages/Index.ets
import { router } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  build() {
    Column() {
      Text('Demo0528')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#3D2B1F')
        .margin({ bottom: 20 })

      Button('进入登录页')
        .width(200)
        .height(48)
        .backgroundColor('#C1793B')
        .border({ width: 1, color: '#C1793B' })
        .borderRadius(24)
        .fontSize(16)
        .fontColor('#FFFFFF')
        .onClick(() => {
          router.pushUrl({
            url: 'pages/LoginPage'
          });
        })
    }
    .backgroundColor('#F3EFE9')
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

4.1 代码逐行解析

代码 作用
import { router } from '@kit.ArkUI' 导入页面路由 API
@Entry 标记该组件为页面入口(配合 pages 配置使用)
@Component 标记为 ArkTS 组件
struct Index 定义组件结构体,名称与文件名无关但与规范推荐一致
Column { ... } 纵向弹性布局容器,子元素从上到下排列
.justifyContent(FlexAlign.Center) 主轴(纵向)居中对齐
.alignItems(HorizontalAlign.Center) 交叉轴(横向)居中对齐
router.pushUrl({ url: 'pages/LoginPage' }) 将 LoginPage 压入页面栈(跳转)

4.2 @Entry 装饰器

@Entry 是页面的标记者。在 ArkTS 中,只有被 @Entry 装饰的组件才能被路由系统识别为独立页面。一个 .ets 文件中可以有多个 @Component,但最多只能有一个 @Entry

🔄 回顾main_pages.json 中注册了 "pages/Index",框架在启动时会自动加载 Index.ets@Entry 装饰的组件作为启动页面。


五、登录页核心构建 LoginPage.ets

这是本文的核心——一个功能完整的登录窗口。它包含了:

  • 品牌 Logo 与标题区域
  • 用户名 / 密码输入框
  • 登录按钮带阴影效果
  • 输入校验逻辑
  • 错误提示
  • 注册链接入口

5.1 完整代码

我们采用 「秋季思雨」 主题配色,下文会专门讲解配色设计思路。先看完整代码:

// entry/src/main/ets/pages/LoginPage.ets
@Entry
@Component
struct LoginPage {
  @State username: string = '';
  @State password: string = '';
  @State isPasswordVisible: boolean = false;
  @State loginMessage: string = '';

  build() {
    Column() {
      // ========== 标题区域 ==========
      Column() {
        Image($r('app.media.startIcon'))
          .width(80)
          .height(80)
          .borderRadius(40)
          .margin({ bottom: 16 })

        Text('欢迎登录')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#3D2B1F')

        Text('请输入账号和密码')
          .fontSize(14)
          .fontColor('#8A9BA8')
          .margin({ top: 8 })
      }
      .alignItems(HorizontalAlign.Center)
      .width('100%')
      .padding({ top: 60, bottom: 40 })

      // ========== 输入区域 ==========
      Column() {
        // ---- 用户名输入框 ----
        Row() {
          TextInput({ placeholder: '请输入用户名', text: this.username })
            .layoutWeight(1)
            .fontSize(16)
            .placeholderColor('#B0A595')
            .placeholderFont({ size: 16 })
            .onChange((value: string) => {
              this.username = value;
            })
        }
        .padding({ left: 16, right: 16 })
        .height(52)
        .backgroundColor('#FDFBF9')
        .border({ width: 1, color: '#D4C5B0' })
        .borderRadius(12)
        .margin({ bottom: 16 })

        // ---- 密码输入框 ----
        Row() {
          TextInput({ placeholder: '请输入密码', text: this.password })
            .layoutWeight(1)
            .fontSize(16)
            .placeholderColor('#B0A595')
            .placeholderFont({ size: 16 })
            .type(InputType.Password)
            .showPasswordIcon(true)
            .onChange((value: string) => {
              this.password = value;
            })
        }
        .padding({ left: 16, right: 16 })
        .height(52)
        .backgroundColor('#FDFBF9')
        .border({ width: 1, color: '#D4C5B0' })
        .borderRadius(12)
      }
      .width('100%')
      .padding({ left: 32, right: 32 })

      // ========== 登录按钮 ==========
      Button('登 录')
        .width('80%')
        .height(50)
        .backgroundColor('#C1793B')
        .border({ width: 1, color: '#C1793B' })
        .borderRadius(25)
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#FFFFFF')
        .margin({ top: 40, bottom: 16 })
        .shadow({
          radius: 8,
          offsetX: 0,
          offsetY: 4,
          color: 'rgba(193, 121, 59, 0.3)'
        })
        .onClick(() => {
          this.handleLogin();
        })

      // ========== 提示消息(条件渲染) ==========
      if (this.loginMessage.length > 0) {
        Text(this.loginMessage)
          .fontSize(14)
          .fontColor('#C94A4A')
          .margin({ top: 8 })
      }

      // ========== 底部注册提示 ==========
      Text('还没有账号?立即注册')
        .fontSize(14)
        .fontColor('#C1793B')
        .margin({ top: 40 })
        .onClick(() => {
          // TODO: 跳转到注册页面
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F3EFE9')
    .alignItems(HorizontalAlign.Center)
  }

  // ========== 登录处理逻辑 ==========
  handleLogin() {
    // 输入校验
    if (this.username.trim() === '') {
      this.loginMessage = '请输入用户名';
      return;
    }
    if (this.password.trim() === '') {
      this.loginMessage = '请输入密码';
      return;
    }

    // 模拟登录验证(后续可替换为真实接口)
    if (this.username === 'admin' && this.password === '123456') {
      this.loginMessage = '';
      console.info('登录成功');
      // TODO: 跳转到主页面
    } else {
      this.loginMessage = '用户名或密码错误';
    }
  }
}

5.2 UI 组件层级分析

Column (page root)
├── Column (header area)
│   ├── Image          ← 品牌 Logo
│   ├── Text           ← "欢迎登录" 标题
│   └── Text           ← "请输入账号和密码" 副标题
├── Column (input area)
│   ├── Row (username)
│   │   └── TextInput  ← 用户名输入框
│   └── Row (password)
│       └── TextInput  ← 密码输入框(.type(InputType.Password))
├── Button             ← "登 录" 按钮
├── Text (conditional) ← 错误提示消息
└── Text               ← "还没有账号?立即注册"

ArkTS 的 UI 就是一个组件树。根节点是 Column(全屏容器),里面按语义划分区域(标题 → 输入 → 按钮 → 提示),每个区域又是一个子 ColumnRow

5.3 状态管理详解

我们定义了四个 @State 变量:

@State username: string = '';           // 用户名
@State password: string = '';           // 密码
@State isPasswordVisible: boolean = false; // 密码可见性(预留)
@State loginMessage: string = '';       // 登录提示消息

状态驱动的数据流

用户输入 → TextInput.onChange → this.username = value
                                     ↓
                               @State username 更新
                                     ↓
                              UI 自动重新渲染
                                     ↓
                              handleLogin() 读取 username
                                     ↓
                              校验 → 通过 → console.info('登录成功')
                                    → 失败 → this.loginMessage = '错误信息'
                                     ↓
                              条件渲染显示错误 → if (this.loginMessage.length > 0)

💡 关键理解:您不需要手动操作 DOM。改变 @State 变量,框架自动找到依赖这些变量的 UI 部分并增量更新。

5.4 输入组件详解

TextInput 的构造方式
TextInput({ placeholder: '请输入用户名', text: this.username })
  • placeholder:占位文本,在输入框为空时显示。
  • text:绑定到 @State 变量,实现受控输入

受控 vs 非受控

ArkTS 的 TextInput 有两种模式:

  1. 受控模式(推荐):传入 text 参数,通过 onChange 回调更新状态。
  2. 非受控模式:不传 text,只通过 onChange 读取输入。

受控模式的优点是状态单一来源——this.username 是唯一真相源,便于后续校验和提交。

密码输入框的特殊配置
.type(InputType.Password)
.showPasswordIcon(true)
  • InputType.Password:将输入内容显示为圆点,保护隐私。
  • .showPasswordIcon(true):在输入框末尾显示一个眼睛图标,用户可以点击切换明文/密文显示。
输入框的视觉设计
Row() {
  TextInput({ placeholder: '请输入用户名', text: this.username })
    .layoutWeight(1)           // 占据所有剩余宽度
    .fontSize(16)
    .placeholderColor('#B0A595')
    .placeholderFont({ size: 16 })
}
.padding({ left: 16, right: 16 })
.height(52)
.backgroundColor('#FDFBF9')
.border({ width: 1, color: '#D4C5B0' })
.borderRadius(12)

设计要点:

  • 输入框被包裹在 Row 中,Row 负责背景色、边框、圆角和内边距。
  • TextInput 本身没有背景色和边框,完全由外层 Row 控制。这种分层设计使样式更容易统一和复用。
  • .height(52) 设置固定行高,保证双端输入框高度一致。
  • .borderRadius(12) 提供柔和的圆角,增强亲和力。

5.5 按钮设计

Button('登 录')
  .width('80%')
  .height(50)
  .backgroundColor('#C1793B')
  .border({ width: 1, color: '#C1793B' })
  .borderRadius(25)
  .fontSize(18)
  .fontWeight(FontWeight.Medium)
  .fontColor('#FFFFFF')
  .margin({ top: 40, bottom: 16 })
  .shadow({
    radius: 8,
    offsetX: 0,
    offsetY: 4,
    color: 'rgba(193, 121, 59, 0.3)'
  })

宽度设计80% 不是全宽,留出左右 10% 的间隙,视觉上更舒适。

圆角按钮borderRadius(25)(高度 50 的一半),形成经典的胶囊按钮。

阴影效果

.shadow({
  radius: 8,       // 阴影模糊半径
  offsetX: 0,      // 水平偏移
  offsetY: 4,      // 垂直偏移(向下)
  color: 'rgba(193, 121, 59, 0.3)'  // 颜色与按钮同色系,降低透明度
})

阴影让按钮有微微浮起的层次感,与秋叶橙的主色呼应。

5.6 登录逻辑 handleLogin()

handleLogin() {
  // 输入校验
  if (this.username.trim() === '') {
    this.loginMessage = '请输入用户名';
    return;
  }
  if (this.password.trim() === '') {
    this.loginMessage = '请输入密码';
    return;
  }

  // 模拟登录验证
  if (this.username === 'admin' && this.password === '123456') {
    this.loginMessage = '';
    console.info('登录成功');
    // TODO: 跳转到主页面
  } else {
    this.loginMessage = '用户名或密码错误';
  }
}

校验流程

  1. 非空校验:先检查用户名,再检查密码。trim() 去除首尾空格。
  2. 凭证校验:目前是硬编码的 admin / 123456,生产环境应替换为网络请求。
  3. 成功处理:清空错误消息 → 打印日志 → 预留跳转。
  4. 失败处理:设置 loginMessage,触发条件渲染显示红色错误提示。

⚠️ 安全提示:在实际项目中,密码校验应在服务端完成,前端仅做格式校验和防抖处理。

5.7 条件渲染错误提示

if (this.loginMessage.length > 0) {
  Text(this.loginMessage)
    .fontSize(14)
    .fontColor('#C94A4A')
    .margin({ top: 8 })
}
  • loginMessage 为空字符串('')时,length > 0false,Text 不渲染。
  • 当登录失败或输入校验不通过时,loginMessage 被赋值为错误文本,Text 自动显示。
  • 错误颜色 #C94A4A 是一种枫叶红色,与秋季主题呼应,而非刺眼的纯红。

5.8 路由导航

首页通过以下代码跳转到登录页:

import { router } from '@kit.ArkUI';

// 在 onClick 中调用
router.pushUrl({
  url: 'pages/LoginPage'
});
  • router.pushUrl 将目标页面推入页面栈,等同于 Android 的 startActivity 或 iOS 的 pushViewController
  • 页面路径相对于 ets/ 目录,不带 .ets 后缀
  • 返回上一页使用 router.back()

六、「秋季思雨」主题配色设计

6.1 设计理念

「秋季思雨」是一种融合季节情感的配色方案。它的灵感来源于:

深秋时节,窗外细雨绵绵。银杏叶在雨中飘落,远处的山峦笼罩在烟灰色的薄雾中。屋内一盏暖灯,空气中弥漫着桂花和泥土的湿润气息。

这种场景传递的情感是:温暖、沉静、怀旧、诗意。我们希望登录页不再是冷冰冰的表单,而是一个有温度、有故事的入口。

6.2 调色板

色名 色值 色相 象征
暖灰米白 #F3EFE9 暖灰 秋日阴天的柔和光线,页面背景
深褐色 #3D2B1F 老树皮、枯枝,标题文字
烟雨灰蓝 #8A9BA8 灰蓝 雨雾、远山,副标题
秋叶橙 #C1793B 枫叶、银杏,主按钮 + 链接
柔米白 #FDFBF9 米白 输入框背景,干净柔和
淡茶色 #D4C5B0 茶棕 输入框边框,低调不抢眼
浅褐灰 #B0A595 灰褐 占位文字,仿佛褪色的记忆
枫叶红 #C94A4A 暗红 错误提示,秋叶转红的警示

6.3 色彩应用规则

页面背景      ← #F3EFE9(暖灰米白——秋日底色)
标题          ← #3D2B1F(深褐——视觉重心)
副标题        ← #8A9BA8(烟雨灰蓝——不喧宾夺主)
输入框背景    ← #FDFBF9(柔白——高于背景一层)
输入框边框    ← #D4C5B0(淡茶——若有若无)
占位文字      ← #B0A595(浅褐——低调提示)
主按钮        ← #C1793B(秋叶橙——温暖号召)
按钮阴影      ← rgba(193, 121, 59, 0.3)
错误提示      ← #C94A4A(枫叶红——温柔警告)
注册链接      ← #C1793B(呼应主色)

6.4 与常见配色方案对比

风格 背景 主色 情感
小清新 薄荷绿 #E8F5E9 翡翠绿 #4CAF50 清爽、活力
科技风 深空蓝 #0A0E27 电光蓝 #00D4FF 现代、冰冷
秋季思雨 暖灰米白 #F3EFE9 秋叶橙 #C1793B 温暖、沉静、诗意

「秋季思雨」的优势在于:它在温暖与克制之间取得了平衡——不会像糖果色那样甜腻,也不会像深色模式那样冷峻。它给人"坐下来,慢慢输入"的心理暗示,降低用户在登录时的焦虑感。


七、ArkTS 进阶话题

7.1 @Component 的生命周期

每个 ArkTS 组件都有生命周期回调:

@Component
struct LoginPage {
  aboutToAppear() {
    console.info('页面即将显示');
    // 可以在这里初始化数据
  }

  onPageShow() {
    console.info('页面显示完成');
  }

  onPageHide() {
    console.info('页面被覆盖');
  }

  aboutToDisappear() {
    console.info('页面即将销毁');
    // 释放资源、取消订阅
  }

  build() {
    // ...
  }
}

在登录页场景中,aboutToAppear 可用于检查用户是否已登录(自动跳过登录页)。

7.2 @State 与 @Prop / @Link

除了 @State,ArkTS 还提供了更多装饰器用于组件通信:

装饰器 作用 适用场景
@State 组件内部状态 当前页面的数据
@Prop 从父组件传入的不可变数据 子组件接收配置
@Link 从父组件传入的双向绑定 子组件可修改父组件数据
@Provide / @Consume 跨层级传递 祖孙组件通信

示例:

// 父组件
@Component
struct Parent {
  @State count: number = 0;

  build() {
    Child({ count: this.count })
  }
}

// 子组件
@Component
struct Child {
  @Prop count: number;

  build() {
    Text(`${this.count}`)
  }
}

7.3 自定义组件复用

我们可以将登录输入框抽取为可复用的组件:

@Component
struct LoginInput {
  @Prop placeholder: string;
  @Prop inputType: InputType = InputType.Normal;
  @Link text: string;

  build() {
    Row() {
      TextInput({ placeholder: this.placeholder, text: this.text })
        .layoutWeight(1)
        .fontSize(16)
        .placeholderColor('#B0A595')
        .placeholderFont({ size: 16 })
        .type(this.inputType)
        .onChange((value: string) => {
          this.text = value;
        })
    }
    .padding({ left: 16, right: 16 })
    .height(52)
    .backgroundColor('#FDFBF9')
    .border({ width: 1, color: '#D4C5B0' })
    .borderRadius(12)
  }
}

然后在 LoginPage 中使用:

LoginInput({ placeholder: '请输入用户名', text: this.username })
LoginInput({ placeholder: '请输入密码', text: this.password, inputType: InputType.Password })

这种抽象带来的好处:

  1. 样式统一:一处修改,处处生效。
  2. 减少重复代码:密码和用户名输入框除了类型外完全相同。
  3. 更易维护:样式变更只需改一个组件。

但注意:对于只有两个实例的场景,是否抽取组件取决于团队的审美偏好。本文为了清晰展示结构,没有抽取,但在生产项目中强烈推荐。

7.4 主题系统

在大型应用中,应该建立统一的主题系统:

// theme.ets
export const Colors = {
  background: '#F3EFE9',
  primary: '#C1793B',
  textPrimary: '#3D2B1F',
  textSecondary: '#8A9BA8',
  inputBg: '#FDFBF9',
  inputBorder: '#D4C5B0',
  placeholder: '#B0A595',
  error: '#C94A4A',
} as const;

export const Spacing = {
  xs: 4,
  sm: 8,
  md: 16,
  lg: 24,
  xl: 40,
} as const;

使用时:

import { Colors, Spacing } from '../common/theme';

Text('欢迎登录')
  .fontColor(Colors.textPrimary)
  .margin({ bottom: Spacing.md })

这种方法在应用有 5 个以上页面时尤其有用,可以避免"改一个颜色要改十几个文件"的困境。

7.5 页面转场动画

ArkTS 支持自定义页面跳转动画:

router.pushUrl({
  url: 'pages/LoginPage'
}, router.RouterMode.Standard, {
  duration: 300,
  curve: Curve.EaseInOut,
  animation: {
    translate: { x: '100%', y: 0 }
  }
});

这是一个从右侧滑入的动画效果。类似 iOS 的 push 转场,可以让应用显得更流畅自然。


八、常见问题与排查

8.1 页面跳转失败

症状:点击「进入登录页」按钮没有任何反应,或报错 page not found

排查步骤

  1. ✅ 检查 main_pages.json 是否注册了目标页面:
{
  "src": [
    "pages/Index",
    "pages/LoginPage"
  ]
}
  1. ✅ 检查 router.pushUrlurl 路径是否正确——不带 .ets 后缀,相对于 ets/ 目录:
// 正确
router.pushUrl({ url: 'pages/LoginPage' });

// 错误(多了后缀)
router.pushUrl({ url: 'pages/LoginPage.ets' });

// 错误(路径不对)
router.pushUrl({ url: 'LoginPage' });
  1. ✅ 检查文件是否存在且被 @Entry 装饰。

8.2 @State 更新但 UI 不刷新

症状loginMessage 变了,但错误文字没有显示。

排查

  1. 确保变量确实被 @State 装饰,如果少写了 @State,ArkTS 不会追踪变化。
  2. 确保 if 条件中的变量是响应式的:
// ✅ 正确
if (this.loginMessage.length > 0) { ... }

// 错误——将响应式变量赋值给局部变量
const msg = this.loginMessage;  // 失去响应性
if (msg.length > 0) { ... }
  1. 不要在 build() 或渲染循环中修改 @State 变量——会导致无限循环。

8.3 输入框样式不生效

症状:设置了 .backgroundColor 但输入框背景没变。

原因TextInput 的默认样式可能覆盖了部分设置。解决方案是将样式设置在外层容器(Row)上,TextInput 本身保持透明。

// ✅ 推荐——样式放在 Row 上
Row() {
  TextInput({ ... })
    .layoutWeight(1)
}
.backgroundColor('#FDFBF9')
.border({ width: 1, color: '#D4C5B0' })
.borderRadius(12)

8.4 页面背景色不生效

症状:设置了 .backgroundColor 但背景还是白色。

原因:容器的背景色只在有内容的区域生效。如果容器本身没有撑满父容器,背景色只会覆盖子元素区域。

解决方案:确保容器设置了宽高为 100%:

Column() {
  // ... 所有内容
}
.width('100%')
.height('100%')
.backgroundColor('#F3EFE9')

九、完整项目结构总结

Demo0528/
├── AppScope/
│   ├── app.json5                    # bundleName: com.shili.Demo0528
│   └── resources/base/media/        # 启动图标、logo
├── entry/
│   ├── build-profile.json5          # apiType: stageMode
│   ├── hvigorfile.ts                # Hvigor 构建脚本
│   └── src/main/
│       ├── ets/
│       │   ├── entryability/
│       │   │   └── EntryAbility.ets # Ability 生命周期
│       │   └── pages/
│       │       ├── Index.ets        # 首页(入口)
│       │       └── LoginPage.ets    # 登录页(核心)
│       ├── module.json5             # 模块配置 + 路由引用
│       └── resources/
│           └── base/
│               ├── element/         # 颜色、字符串等资源
│               └── profile/
│                   └── main_pages.json  # 页面路由表
├── build-profile.json5              # 应用级构建配置
├── hvigor/                          # 构建缓存
└── oh-package.json5                 # OHPM 依赖

产出文件清单

文件 行数 核心内容
Index.ets 34 首页布局+路由跳转
LoginPage.ets 146 完整的登录表单+校验逻辑
main_pages.json 6 页面注册
module.json5 50 模块信息+Ability配置

十、下一步拓展方向

本文实现的登录窗口虽然功能完整,但仍有许多可以延展的方向:

10.1 接入真实后端 API

import { http } from '@kit.Networking';

async handleLogin() {
  const httpRequest = http.createHttp();
  const response = await httpRequest.request('https://api.example.com/login', {
    method: http.RequestMethod.POST,
    header: { 'Content-Type': 'application/json' },
    extraData: {
      username: this.username,
      password: this.password
    }
  });
  if (response.responseCode === 200) {
    // 登录成功,保存 token
    AppStorage.setOrCreate('token', response.result.token);
    router.pushUrl({ url: 'pages/HomePage' });
  } else {
    this.loginMessage = '登录失败,请检查网络或账号密码';
  }
  httpRequest.destroy();
}

10.2 注册页面

实现「还没有账号?立即注册」的跳转,创建 RegisterPage.ets,在 main_pages.json 中注册。

10.3 记住密码与生物识别

import { preferences } from '@kit.ArkData';
import { userAuth } from '@kit.UserAuthenticationKit';

// 存储账号
const pref = await preferences.getPreferences(this.context, 'loginPrefs');
await pref.put('savedUsername', this.username);
await pref.flush();

// 指纹/面容识别
const authResult = await userAuth.auth();
if (authResult.isSuccess) {
  // 自动填充密码并登录
}

10.4 表单验证增强

private validateUsername(): boolean {
  if (this.username.trim().length < 3) {
    this.loginMessage = '用户名至少3个字符';
    return false;
  }
  if (!/^[a-zA-Z0-9_]+$/.test(this.username)) {
    this.loginMessage = '用户名只能包含字母、数字和下划线';
    return false;
  }
  return true;
}

private validatePassword(): boolean {
  if (this.password.length < 6) {
    this.loginMessage = '密码至少6位';
    return false;
  }
  return true;
}

10.5 多主题切换

@StorageLink('themeMode') themeMode: string = 'autumn';

// 根据主题动态选择颜色
get primaryColor(): string {
  const themes = {
    autumn: '#C1793B',
    tech: '#00D4FF',
    fresh: '#4CAF50'
  };
  return themes[this.themeMode] || themes.autumn;
}

十一、结语

从零开始构建一个登录窗口,我们走过了这样的旅程:

  1. 理解项目结构:Stage Model 的模块划分、配置文件的作用。
  2. 掌握 ArkTS 声明式 UI@Component@Statebuild() 的协作。
  3. 构建完整页面:标题区 → 输入区 → 按钮 → 提示,逐层搭建。
  4. 实现交互逻辑:onChange 绑定 → 状态驱动 → handleLogin 校验。
  5. 设计主题配色:「秋季思雨」——用色彩传递产品情感。
  6. 探索进阶话题:组件通信、生命周期、主题系统、项目优化。

ArkTS 作为鸿蒙原生的声明式框架,其设计理念借鉴了现代前端和移动端框架的最佳实践,同时融合了鸿蒙生态的独特能力。如果您有 React / Vue / SwiftUI / Jetpack Compose 经验,上手会非常顺畅;即使没有,ArkTS 的简洁语法和完整的文档体系也能让您快速进入状态。

最后留一个思考题给您:

如果要将这个登录页改为「多语言支持」(中文/英文/日文),你会如何设计 ArkTS 的国际化方案?欢迎尝试实现并分享你的方案。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐