鸿蒙 HarmonyOS 校园风登录页面开发实战 —— 基于 ArkTS 的 Stage 模型完整教程

一、前言

随着国产操作系统的蓬勃发展,华为 HarmonyOS(鸿蒙系统)已经逐渐成为移动端、平板端乃至物联网设备的重要选择。对于前端开发者而言,鸿蒙提供了 ArkTS(方舟 TypeScript)作为主力开发语言,它基于 TypeScript 语法并扩展了声明式 UI 描述能力,让页面开发既高效又优雅。

在校园类应用开发中,登录页面 是用户接触产品的第一张面孔。一个设计精良、风格统一的登录页面不仅能提升用户体验,更能传达校园文化的精神内核。本文将从零到一,手把手带你使用 HarmonyOS 的 ArkTS 语言和 Stage 模型,打造一个 清新校园风 的登录窗口。

无论你是刚刚接触鸿蒙开发,还是有一定经验想要系统了解登录页面的最佳实践,这篇文章都将给你带来干货满满的内容。全文约 10000 字,涵盖环境搭建、项目结构、UI 设计、路由跳转、状态管理、样式细节等方方面面,所有代码均可直接运行。


二、HarmonyOS 与 ArkTS 简介

2.1 什么是 HarmonyOS

HarmonyOS(鸿蒙系统)是华为开发的一款面向全场景的分布式操作系统。它的核心理念是 “一生万物,万物归一”,通过统一的开发框架,让应用可以在手机、平板、手表、智慧屏、车机等多种设备上无缝运行。

2.2 Stage 模型

从 HarmonyOS 3.0 开始,Stage 模型成为主推的应用开发模型。与早期的 FA(Feature Ability)模型相比,Stage 模型有以下显著优势:

特性 FA 模型 Stage 模型
组件管理 Ability 即页面 Ability + 页面分离
生命周期 较简单 更丰富精细
共享数据 使用 GlobalThis 使用 AppScope + 共享上下文
模块化 较弱 支持 HAP 分包,模块化强
推荐程度 已逐渐弃用 当前主流推荐

2.3 ArkTS 语言

ArkTS 是鸿蒙生态的主力开发语言,它在 TypeScript 的基础上做了以下增强:

  • 声明式 UI:使用 @Component + build() 描述界面结构
  • 状态管理:使用 @State@Prop@Link 等装饰器管理组件状态
  • 双向数据绑定:支持 $$ 语法实现数据的快速绑定
  • 强类型约束:继承了 TypeScript 的静态类型检查,减少运行时错误
  • 性能优化:ArkTS 编译器会对代码进行 AOT(Ahead-of-Time)编译,运行时性能接近原生

一个简单的 ArkTS 组件结构如下:

@Component
struct MyComponent {
  @State count: number = 0

  build() {
    Column() {
      Text(`计数: ${this.count}`)
        .fontSize(24)
      Button('点击 +1')
        .onClick(() => {
          this.count++
        })
    }
    .width('100%')
    .height('100%')
  }
}

三、项目环境搭建

3.1 开发工具:DevEco Studio

鸿蒙应用开发的首选 IDE 是 DevEco Studio,它是华为基于 IntelliJ IDEA 定制开发的集成开发环境。建议下载最新版本(当前推荐 5.0+)。

下载地址:华为开发者联盟 - DevEco Studio

3.2 创建项目

打开 DevEco Studio 后,按以下步骤创建项目:

  1. 点击 File → New → Create Project
  2. 选择模板:Empty Ability(空模板)
  3. 填写项目信息:
    • Project NameCampusLoginDemo
    • Bundle Namecom.example.campuslogindemo
    • Save Location:自定义
    • Compile SDK:选择 4.0 或更高版本
    • Model:选择 Stage 模型
    • Language:选择 ArkTS
  4. 点击 Finish,等待项目初始化完成

项目创建后的目录结构如下:

CampusLoginDemo/
├── AppScope/                  # 应用级配置
│   └── app.json5              # 应用全局配置
├── entry/                     # 主 Entry 模块
│   ├── src/
│   │   ├── main/
│   │   │   ├── ets/           # ArkTS 源码
│   │   │   │   ├── entryability/
│   │   │   │   │   └── EntryAbility.ets
│   │   │   │   └── pages/
│   │   │   │       └── Index.ets
│   │   │   ├── resources/     # 资源文件
│   │   │   │   ├── base/
│   │   │   │   │   ├── element/
│   │   │   │   │   ├── media/
│   │   │   │   │   ├── profile/
│   │   │   │   │   │   └── main_pages.json
│   │   │   │   │   └── string.json
│   │   │   │   └── ...
│   │   │   └── module.json5   # 模块配置
│   │   └── ...
│   └── build-profile.json5
├── oh_modules/                # 依赖模块
├── build-profile.json5        # 全局构建配置
└── hvigorfile.ts              # 构建脚本

3.3 关键配置文件说明

module.json5 —— 模块配置
{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone"],
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["ohos.want.action.home"]
          }
        ]
      }
    ]
  }
}

这里的关键点是:

  • srcEntry 指向 Ability 的入口文件
  • pages 指向 $profile:main_pages,即 resources/base/profile/main_pages.json
  • skills 中的 entity.system.home + ohos.want.action.home 表示这是桌面首页入口
main_pages.json —— 页面路由表
{
  "src": [
    "pages/Index",
    "pages/Login"
  ]
}

这个文件定义了应用中的所有页面路由路径。每当新增一个页面时,都必须在这里注册,否则应用无法找到对应的页面资源导致跳转失败。


四、校园风登录页面设计思路

4.1 什么是"校园风"

在动手写代码之前,先来明确"校园风"的设计语言。校园风格的核心关键词是:

关键词 设计体现
🌿 青春 明亮的色彩、圆润柔和的形状
📚 学术 学士帽、书本元素、校训文字
🏫 校园 绿色调(象征树木和草坪)
清新 留白充足、卡片式布局、柔和阴影
🤝 亲切 友好的文案、引导性交互

4.2 配色方案

我们为校园风登录页面选择了以下配色:

主色:   #4CAF50   —— 草木绿,代表校园的自然气息
深绿:   #2E7D32   —— 用于主标题,稳重
浅绿:   #81C784   —— 辅助文字、链接
背景:   #E8F5E9   —— 极浅绿色,给人宁静、舒适感
输入框: #F1F8E9   —— 极浅米绿,区分卡片背景
按钮禁用:#C8E6C9   —— 按钮不可用状态的浅绿
卡片阴影:#1A4CAF50 —— 带透明度的绿色阴影

这套配色方案的核心原则:

  1. 低饱和度 —— 避免刺眼的高饱和色,校园风追求柔和
  2. 同色系渐变 —— 从 #E8F5E9#4CAF50#2E7D32,色彩层次分明但不跳脱
  3. 绿色为主,白色为辅 —— 和自然校园环境吻合

4.3 布局结构

页面采用自上而下的垂直布局,共分为三个层次:

┌──────────────────────────────────┐
│          顶部徽章区域               │  ← 校徽 + 校名 + 校训
│                                   │
│  ┌────────────────────────────┐  │
│  │       表单卡片区域           │  │  ← 白色卡片,悬浮效果
│  │  ┌──────────────────────┐  │  │
│  │  │ 学号 / 用户名输入框    │  │  │
│  │  ├──────────────────────┤  │  │
│  │  │ 密码输入框 + 可见切换  │  │  │
│  │  ├──────────────────────┤  │  │
│  │  │ 忘记密码链接          │  │  │
│  │  ├──────────────────────┤  │  │
│  │  │ 登录按钮              │  │  │
│  │  ├──────────────────────┤  │  │
│  │  │ 注册引导文字          │  │  │
│  │  └──────────────────────┘  │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘

层的优点:

  • 视觉聚焦:用户的视线从顶部校徽 → 中间表单 → 底部按钮,路径清晰
  • 操作流畅:从上到下依次填写表单,符合自然操作流程
  • 适配性好:使用 Scroll 包裹,在屏幕较小的设备上也能正常显示全部内容

4.4 交互细节

好的登录页面不止好看,还要好"用"。以下是本页面实现的交互细节:

  1. 输入框聚焦态:未设计复杂的边框变化,但浅绿背景在输入时能自然引导
  2. 密码可见切换:点击 👁 图标切换密码明文/密文显示,提升输入体验
  3. 按钮状态联动:用户名和密码都非空时按钮才高亮可用,直观反馈
  4. 按钮置灰禁用:不符合条件时按钮呈浅灰色,视觉上不可点击

五、代码实现 —— 登录页面详解

5.1 完整代码

@Entry
@Component
struct Login {
  @State username: string = '';
  @State password: string = '';
  @State passwordVisible: boolean = false;

  build() {
    Column() {
      Scroll() {
        Column() {
          // ===== 顶部校园徽章区域 =====
          Column() {
            // 校徽图案(用形状拼一个学士帽风格图标)
            Stack() {
              Circle()
                .width(80)
                .height(80)
                .fill('#4CAF50')

              Column() {
                Text('🎓')
                  .fontSize(36)
              }
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
            }
            .width(80)
            .height(80)
            .margin({ bottom: 16 })

            Text('智慧校园')
              .fontSize(26)
              .fontWeight(FontWeight.Bold)
              .fontColor('#2E7D32')

            Text('立德树人 · 知行合一')
              .fontSize(13)
              .fontColor('#81C784')
              .margin({ top: 6 })
              .letterSpacing(4)
          }
          .width('100%')
          .margin({ top: 60, bottom: 50 })

          // ===== 表单卡片 =====
          Column() {
            // 表单标题
            Text('账号登录')
              .fontSize(20)
              .fontWeight(FontWeight.Medium)
              .fontColor('#1a1a1a')
              .width('100%')
              .margin({ bottom: 28 })

            // 学号 / 用户名
            Column() {
              Row() {
                Text('📖')
                  .fontSize(16)
                  .margin({ right: 6 })
                Text('学号 / 用户名')
                  .fontSize(14)
                  .fontColor('#558B2F')
              }
              .margin({ bottom: 8 })

              TextInput({ placeholder: '请输入学号或用户名', text: this.username })
                .onChange((value: string) => {
                  this.username = value
                })
                .fontSize(16)
                .placeholderColor('#bdbdbd')
                .padding({ left: 16, right: 16 })
                .width('100%')
                .height(48)
                .backgroundColor('#F1F8E9')
                .borderRadius(12)
            }
            .width('100%')
            .margin({ bottom: 20 })

            // 密码
            Column() {
              Row() {
                Text('🔒')
                  .fontSize(16)
                  .margin({ right: 6 })
                Text('密码')
                  .fontSize(14)
                  .fontColor('#558B2F')
              }
              .margin({ bottom: 8 })

              Row() {
                TextInput({ placeholder: '请输入密码', text: this.password })
                  .type(InputType.Password)
                  .onChange((value: string) => {
                    this.password = value
                  })
                  .fontSize(16)
                  .placeholderColor('#bdbdbd')
                  .layoutWeight(1)
                Text(this.passwordVisible ? '🙈' : '👁')
                  .fontSize(18)
                  .onClick(() => {
                    this.passwordVisible = !this.passwordVisible
                  })
              }
              .padding({ left: 16, right: 16 })
              .width('100%')
              .height(48)
              .backgroundColor('#F1F8E9')
              .borderRadius(12)
            }
            .width('100%')
            .margin({ bottom: 12 })

            // 忘记密码
            Row() {
              Blank()
              Text('忘记密码?')
                .fontSize(13)
                .fontColor('#81C784')
                .onClick(() => {
                  console.log('跳转到忘记密码')
                })
            }
            .width('100%')
            .margin({ bottom: 32 })

            // 登录按钮
            Button('登  录')
              .type(ButtonType.Capsule)
              .width('100%')
              .height(50)
              .backgroundColor(this.username.length > 0 && this.password.length > 0
                ? '#4CAF50' : '#C8E6C9')
              .fontSize(18)
              .fontWeight(FontWeight.Medium)
              .fontColor(Color.White)
              .enabled(this.username.length > 0 && this.password.length > 0)
              .onClick(() => {
                console.log(`登录:学号=${this.username}, 密码=${this.password}`)
              })

            // 注册引导
            Row() {
              Text('还没有校园账号?')
                .fontSize(14)
                .fontColor('#9E9E9E')
              Text('立即注册')
                .fontSize(14)
                .fontColor('#4CAF50')
                .fontWeight(FontWeight.Medium)
                .margin({ left: 4 })
                .onClick(() => {
                  console.log('跳转到注册')
                })
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .margin({ top: 24 })
          }
          .width('100%')
          .padding(28)
          .backgroundColor(Color.White)
          .borderRadius(20)
          .shadow({
            radius: 20,
            color: '#1A4CAF50',
            offsetY: 4
          })
        }
        .width('100%')
        .padding({ left: 28, right: 28 })
      }
      .width('100%')
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#E8F5E9')
  }
}

5.2 代码分块详解

下面逐块分析代码的核心设计。

5.2.1 组件定义与状态管理
@Entry
@Component
struct Login {
  @State username: string = '';
  @State password: string = '';
  @State passwordVisible: boolean = false;
  • @Entry:标记该组件为页面的入口组件,每个页面有且只有一个 @Entry 组件
  • @Component:声明这是一个自定义组件
  • @State:状态装饰器。当被修饰的变量值发生变化时,UI 会自动重新渲染。这里三个状态变量分别管理:
    • username:绑定用户名输入框的文本
    • password:绑定密码输入框的文本
    • passwordVisible:控制密码是否明文显示

注意:ArkTS 中 @State 只能修饰简单类型(string, number, boolean)或简单对象/数组,不支持复杂嵌套对象的深度监听。

5.2.2 根布局
build() {
  Column() {
    Scroll() {
      // ...
    }
    .scrollBar(BarState.Off)
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#E8F5E9')
}
  • Column:垂直布局容器,所有子组件从上到下排列
  • Scroll:滚动容器。当内容高度超过屏幕高度时,用户可以向下滑动查看被遮挡的内容。这在登录页中非常重要,因为某些小屏设备可能无法一屏显示完整的表单。
  • BarState.Off:隐藏滚动条,保持界面整洁
  • 根背景色 #E8F5E9:极浅的绿色,奠定校园清新的基调
5.2.3 顶部徽章区域
Column() {
  Stack() {
    Circle()
      .width(80).height(80)
      .fill('#4CAF50')
    Column() {
      Text('🎓').fontSize(36)
    }
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
  .width(80).height(80)
  .margin({ bottom: 16 })

  Text('智慧校园')
    .fontSize(26)
    .fontWeight(FontWeight.Bold)
    .fontColor('#2E7D32')

  Text('立德树人 · 知行合一')
    .fontSize(13)
    .fontColor('#81C784')
    .margin({ top: 6 })
    .letterSpacing(4)
}

设计细节:

  • Stack 层叠布局:将绿色圆形(Circle)作为底层,🎓 Emoji 作为顶层居中显示,组合成校徽效果
  • 标题 #2E7D32:深绿色传递稳重、学术的感觉
  • 校训 #81C784:浅绿色不抢眼,但 letterSpacing(4) 增加字符间距,提升文字品质感
  • “立德树人 · 知行合一”:这是常用的校训句式,中间用 · 分隔,既古雅又现代

为什么要用 Emoji 做校徽? 在原型阶段,使用 Emoji 可以快速验证布局效果,无需额外准备图标资源。正式发布时,可以替换为设计团队提供的 PNG/SVG 校徽图片。

5.2.4 表单卡片
Column() {
  // ...
}
.padding(28)
.backgroundColor(Color.White)
.borderRadius(20)
.shadow({
  radius: 20,
  color: '#1A4CAF50',
  offsetY: 4
})

表单卡片的设计细节:

  • borderRadius(20):20px 圆角,让卡片看起来柔和亲切
  • shadow:阴影效果,参数解释:
    • radius: 20 —— 阴影模糊半径,值越大阴影越柔和
    • color: '#1A4CAF50' —— 绿色透明度 10%(1A 是十六进制透明度值),让阴影带有环境色而不仅仅是灰色
    • offsetY: 4 —— 垂直向下偏移 4px,模拟自然光照从上方照射的效果

这种 “漂浮卡片” 设计是现代移动端 UI 的经典手法,它通过阴影将内容"抬离"背景,形成清晰的视觉层次。

5.2.5 输入框组件

我们以用户名输入框为例:

Column() {
  // 标签行
  Row() {
    Text('📖').fontSize(16).margin({ right: 6 })
    Text('学号 / 用户名').fontSize(14).fontColor('#558B2F')
  }
  .margin({ bottom: 8 })

  TextInput({ placeholder: '请输入学号或用户名', text: this.username })
    .onChange((value: string) => {
      this.username = value
    })
    .fontSize(16)
    .placeholderColor('#bdbdbd')
    .padding({ left: 16, right: 16 })
    .width('100%')
    .height(48)
    .backgroundColor('#F1F8E9')
    .borderRadius(12)
}

设计考量:

  1. 标签与输入框分离:标签在上方(带小图标),输入框在下方。这种上下结构在移动端比左右结构更适合小屏幕阅读。

  2. TextInput 属性

    • placeholder:占位提示文字,引导用户输入
    • onChange:输入变化回调,同步更新 this.username
    • backgroundColor('#F1F8E9'):输入框使用比卡片背景略深的米绿色,形成层次感
    • borderRadius(12):12px 圆角,配合卡片的 20px 圆角形成视觉节奏
  3. 双向数据绑定{ text: this.username } 将状态变量绑定到输入框的显示文本;onChange 将用户输入写回状态变量。这是"单向数据流 + 事件回调"的模式,也是 ArkTS 推荐的做法。

5.2.6 密码输入与可见切换

密码输入框与用户名输入框类似,但有三个特殊之处:

// 1. 输入类型设为密码
TextInput({ placeholder: '请输入密码', text: this.password })
  .type(InputType.Password)

// 2.「忘记密码」链接
Row() {
  Blank()
  Text('忘记密码?')
    // ...
}

// 3. 密码可见切换按钮
Text(this.passwordVisible ? '🙈' : '👁')
  .fontSize(18)
  .onClick(() => {
    this.passwordVisible = !this.passwordVisible
  })
  • InputType.Password:输入内容显示为圆点,保护隐私
  • Blank():占位撑开右侧空间,让"忘记密码?"文字靠右对齐
  • 密码切换:通过 passwordVisible 布尔值切换显示图标(👁/🙈),但注意这里只是视觉上的切换图标,密码的明文/密文切换需要通过修改 TextInputtype 属性实现。完整的实现可以这样改进:
TextInput({ placeholder: '请输入密码', text: this.password })
  .type(this.passwordVisible ? InputType.Normal : InputType.Password)

passwordVisibletrue 时输入框类型为 Normal(明文显示),否则为 Password(密文显示)。

5.2.7 登录按钮的状态联动
Button('登  录')
  .type(ButtonType.Capsule)
  .width('100%')
  .height(50)
  .backgroundColor(this.username.length > 0 && this.password.length > 0
    ? '#4CAF50' : '#C8E6C9')
  .fontColor(Color.White)
  .enabled(this.username.length > 0 && this.password.length > 0)
  .onClick(() => {
    console.log(`登录:学号=${this.username}, 密码=${this.password}`)
  })

这是整个页面最关键的用户体验细节——按钮状态联动

  • 条件判断this.username.length > 0 && this.password.length > 0
  • 视觉联动:条件满足时按钮为亮绿色 #4CAF50,否则为浅灰色 #C8E6C9
  • 行为联动enabled() 属性控制按钮是否可点击,条件不满足时禁用点击

这种设计有两大好处:

  1. 防止空表单提交:用户在未填写完整信息时无法提交,减少无效请求
  2. 即时反馈:用户每输入一个字符,按钮状态就变化一次,交互感强

进阶思考:实际项目中,还可以增加对输入格式的校验,比如学号是否为数字、密码长度是否≥6位等,进一步提升体验。

5.2.8 注册引导
Row() {
  Text('还没有校园账号?')
    .fontSize(14)
    .fontColor('#9E9E9E')
  Text('立即注册')
    .fontSize(14)
    .fontColor('#4CAF50')
    .fontWeight(FontWeight.Medium)
    .margin({ left: 4 })
    .onClick(() => {
      console.log('跳转到注册')
    })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 24 })

这是一个典型的 “引导 + 行动号召” 模式:

  • 灰色文字降低存在感,不会干扰主要登录流程
  • 绿色"立即注册"作为号召性用语,视觉突出
  • FlexAlign.Center 居中显示,置于按钮下方,符合阅读顺序

六、页面路由配置

6.1 首页(Index.ets)

要让登录页面能被访问到,我们需要在首页添加一个跳转按钮。

import router from '@ohos.router';

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.message = 'Welcome';
        })

      Button('登录')
        .type(ButtonType.Capsule)
        .height(48)
        .width(120)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .backgroundColor('#007aff')
        .fontColor(Color.White)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .margin({ top: 80 })
        .onClick(() => {
          router.pushUrl({ url: 'pages/Login' })
            .catch((err: Error) => {
              console.error(`跳转失败: ${err.message}`)
            })
        })
    }
    .height('100%')
    .width('100%')
  }
}

关键点:

  • import router from '@ohos.router':导入路由器模块
  • router.pushUrl({ url: 'pages/Login' }):使用 pushUrl 方法跳转到 Login 页面
  • catch 错误处理:如果跳转失败(比如路由表中没有 Login 页面),会在控制台输出错误信息

6.2 路由表配置

路由表文件位于 entry/src/main/resources/base/profile/main_pages.json

{
  "src": [
    "pages/Index",
    "pages/Login"
  ]
}

重要提醒:每新增一个页面,必须在此文件中注册。路径规则:

  • 路径相对于 ets/ 目录
  • "pages/Login" 对应 ets/pages/Login.ets
  • 文件名不带 .ets 后缀

如果不注册路由表,编译阶段不会报错,但运行时 router.pushUrl 会抛出类似以下错误:

跳转失败: The page URI is not exist in the page route list.

6.3 路由跳转方式对比

方法 说明 适用场景
router.pushUrl 压栈跳转,保留当前页面 页面层次导航(如首页→登录→详情)
router.replaceUrl 替换当前页面,不保留 登录成功后替换首页
router.back 返回上一页 取消/返回操作
router.clear 清空页面栈 退出登录回到首页

对于登录页面的场景,通常使用 pushUrl 跳转到登录页,登录成功后再使用 replaceUrl 跳转到主界面(这样用户按返回键不会回到登录页)。


七、样式设计的深度解析

7.1 颜色体系

校园风的颜色体系基于 绿色色轮 进行设计:

                        #2E7D32 (深绿 - 标题)
                           ↑ 更暗
    #C8E6C9 (禁用按钮) ← #4CAF50 (主色 - 校徽/按钮)
                           ↓ 更亮
                        #81C784 (辅助文字/链接)
                           ↓ 更亮
                        #F1F8E9 (输入框背景)
                           ↓ 更亮
                        #E8F5E9 (页面背景)

通过调整绿色的明度和饱和度,我们可以用单一色相营造出丰富的视觉层次,既有统一感,又有清晰的信息层级。

7.2 间距系统

一个好的页面需要"呼吸感",间距至关重要。本页面采用的间距体系:

位置 间距值 设计意图
顶部与徽章 60px 让徽章靠上但不贴顶,有仪式感
徽章与表单 50px 两个区域的明显分隔
表单内部 28px padding 内容不贴卡边
输入框之间 20px 足够的垂直间距避免误触
标签与输入框 8px 紧密的关联性

7.3 字体系统

元素 字号 字重 颜色
校名标题 26px Bold #2E7D32
校训 13px Regular #81C784
表单标题 20px Medium #1a1a1a
输入框标签 14px Regular #558B2F
输入内容 16px Regular 默认
占位文字 16px Regular #bdbdbd
按钮文字 18px Medium #FFFFFF
忘记密码 13px Regular #81C784
注册引导 14px Regular #9E9E9E

字号的递进关系(13 → 14 → 16 → 18 → 20 → 26)形成了清晰的视觉节奏,让用户一眼就能区分内容的层级。

7.4 圆角系统

圆形校徽:    100% 圆形 (80x80)
表单卡片:    20px
输入框:      12px
登录按钮:    Capsule (胶囊 = 完全圆角)

圆角的"递进"也是有规律的:卡片 20px > 输入框 12px > 校徽完全圆形。最外层最圆润,内层略小,形成视觉上的嵌套关系和统一感。


八、「忘记密码」和「密码可见切换」完善方案

在浏览器的 Demo 中,密码可见切换的图标虽然变了,但 TextInput 的类型没有联动变化。下面是完善后的版本:

// 密码输入框区域 —— 完善版
Column() {
  Row() {
    Text('🔒').fontSize(16).margin({ right: 6 })
    Text('密码').fontSize(14).fontColor('#558B2F')
  }
  .margin({ bottom: 8 })

  Row() {
    TextInput({ placeholder: '请输入密码', text: this.password })
      .type(this.passwordVisible ? InputType.Normal : InputType.Password)  // ← 联动
      .onChange((value: string) => {
        this.password = value
      })
      .fontSize(16)
      .placeholderColor('#bdbdbd')
      .layoutWeight(1)
    Text(this.passwordVisible ? '🙈' : '👁')
      .fontSize(18)
      .onClick(() => {
        this.passwordVisible = !this.passwordVisible  // ← 切换状态
      })
  }
  .padding({ left: 16, right: 16 })
  .width('100%')
  .height(48)
  .backgroundColor('#F1F8E9')
  .borderRadius(12)
}

而"忘记密码"通常需要跳转到专门的找回密码页面。我们可以提前在路由表中注册 pages/ForgetPassword,然后在点击事件中执行跳转:

Text('忘记密码?')
  .fontSize(13)
  .fontColor('#81C784')
  .onClick(() => {
    router.pushUrl({ url: 'pages/ForgetPassword' })
      .catch((err: Error) => {
        console.error(`跳转忘记密码页面失败: ${err.message}`)
      })
  })

九、让校徽更精致:使用 SVG 或 Media 资源

前面我们使用 Emoji(🎓)作为校徽图标,但在生产环境中通常需要替换为真实的校徽图片。

9.1 准备图片资源

将校徽图片(建议使用 PNG 或 SVG 格式)放入项目的资源目录:

entry/src/main/resources/base/media/
├── app_icon.png          # 应用图标
├── logo_school.png       # 校徽图片(新建)
└── ...

9.2 替换校徽代码

// 使用 Image 组件替换 Stack + Emoji
Stack() {
  Circle()
    .width(80)
    .height(80)
    .fill('#4CAF50')

  Image($r('app.media.logo_school'))   // 引用资源文件
    .width(48)
    .height(48)
    .objectFit(ImageFit.Contain)
}
.width(80)
.height(80)
.margin({ bottom: 16 })

$r('app.media.logo_school') 是鸿蒙的资源引用语法,编译时会自动解析文件路径并对不同 DPI 设备做适配。

9.3 资源适配

如果应用需要适配不同分辨率的设备,可以在 media 目录下提供多套资源:

media/
├── logo_school.png         # 默认(160 dpi)
├── ldpi/
│   └── logo_school.png     # 120 dpi
├── mdpi/
│   └── logo_school.png     # 160 dpi
├── hdpi/
│   └── logo_school.png     # 240 dpi
├── xhdpi/
│   └── logo_school.png     # 320 dpi
└── xxhdpi/
    └── logo_school.png     # 480 dpi

十、扩展:添加表单校验

生产环境中,登录页面通常需要做输入校验。下面展示如何在现有代码基础上增加校验逻辑。

10.1 增加错误状态

@Entry
@Component
struct Login {
  @State username: string = '';
  @State password: string = '';
  @State passwordVisible: boolean = false;
  @State usernameError: string = '';   // 用户名错误提示
  @State passwordError: string = '';   // 密码错误提示
}

10.2 校验函数

validate(): boolean {
  let valid = true

  // 清空之前的错误
  this.usernameError = ''
  this.passwordError = ''

  // 校验学号:必须为数字且长度≥6
  if (this.username.length === 0) {
    this.usernameError = '请输入学号'
    valid = false
  } else if (!/^\d{6,}$/.test(this.username)) {
    this.usernameError = '学号必须为6位以上数字'
    valid = false
  }

  // 校验密码:长度≥6
  if (this.password.length === 0) {
    this.passwordError = '请输入密码'
    valid = false
  } else if (this.password.length < 6) {
    this.passwordError = '密码长度不能少于6位'
    valid = false
  }

  return valid
}

10.3 在登录按钮中使用校验

Button('登  录')
  // ... 其他属性不变
  .onClick(() => {
    if (this.validate()) {
      console.log(`登录:学号=${this.username}, 密码=${this.password}`)
      // 执行实际的登录请求...
    }
  })

10.4 在 UI 中显示错误

TextInput({ placeholder: '请输入学号或用户名', text: this.username })
  // ... 其他属性
  .onChange((value: string) => {
    this.username = value
    this.usernameError = ''  // 用户重新输入时清除错误
  })

// 显示用户名错误
if (this.usernameError.length > 0) {
  Text(this.usernameError)
    .fontSize(12)
    .fontColor('#F44336')   // 红色醒目标识
    .margin({ top: 4, left: 4 })
}

这样用户在提交时就能获得明确的错误反馈,而不是无响应或闪退。


十一、常见问题与排错

11.1 路由跳转失败

现象:点击按钮后控制台输出 跳转失败: The page URI is not exist...

原因:登录页面未在 main_pages.json 中注册。

解决:检查 main_pages.json 是否包含 "pages/Login"

{
  "src": [
    "pages/Index",
    "pages/Login"
  ]
}

11.2 按钮始终禁用

现象:输入用户名和密码后按钮颜色不变化

原因:状态变量未正确绑定或 onChange 回调未更新状态

检查

  1. 确认 @State 装饰器已添加
  2. 确认 TextInputonChange 回调中更新了状态变量
@State username: string = ''
// ...
TextInput({
  placeholder: '请输入学号或用户名',
  text: this.username      // 绑定了状态变量的值
})
.onChange((value: string) => {
  this.username = value    // 更新状态变量
})

11.3 密码可见切换不生效

现象:点击 👁 图标后图标变了,但密码仍然显示为圆点

原因TextInputtype 属性未随状态变化

解决:确保 type 属性使用状态变量的条件表达式:

TextInput({ placeholder: '请输入密码', text: this.password })
  .type(this.passwordVisible ? InputType.Normal : InputType.Password)

11.4 灰色阴影看不到

原因shadowcolor 属性使用了带透明度的颜色(如 #1A4CAF50),在浅色背景下可能不够明显。

解决:增大颜色透明度值(如将 #1A 改为 #33),或增大 radius 值:

.shadow({
  radius: 30,              // 增大模糊半径
  color: '#334CAF50',      // 增大不透明度
  offsetY: 6               // 增大偏移量
})

十二、总结

本文从零开始,完整地实现了一个鸿蒙 HarmonyOS 校园风登录页面。从项目初始化到页面设计,从状态管理到路由跳转,从交互细节到表单校验,覆盖了 ArkTS 开发登录页面的全流程。

本文知识点回顾

知识点 详细内容
ArkTS 组件基础 @Entry, @Component, build()
状态管理 @State 装饰器的使用
布局容器 Column, Row, Stack, Scroll
基础组件 Text, TextInput, Button, Image, Circle
样式设计 颜色、圆角、阴影、间距系统
路由跳转 router.pushUrl, main_pages.json 配置
交互设计 按钮状态联动、密码可见切换、输入校验
资源管理 $r() 引用图片、多 DPI 适配

设计哲学

好的校园风设计应该是 “润物细无声” 的——绿色调让人联想到绿树成荫的校园小径,圆角让界面像校园氛围一样柔和亲切,清晰的校徽和校训传达着学校的文化传承。技术服务于设计,设计服务于体验,这正是 UI 开发的魅力所在。

下一步可以做什么?

完成登录页面后,你可以继续扩展:

  1. 注册页面:与登录页面风格保持一致,添加更多表单字段
  2. 忘记密码页面:实现验证码发送和密码重置流程
  3. 记住我功能:使用 PersistStorage 进行本地存储
  4. 生物识别登录:调用系统指纹/面部识别 API
  5. 多语言适配:使用 $r() 引用字符串资源,支持中英文切换

鼓励的话

鸿蒙生态正在蓬勃发展,学习 ArkTS 开发不仅是掌握一门技术,更是参与国产操作系统生态建设的过程。希望这篇文章能帮助你在鸿蒙开发的道路上走得更远。

如果你觉得本文对你有帮助,欢迎收藏和分享,也欢迎在评论区留下你的建议和问题。让我们一起,用代码构建更美好的校园数字生活!


本文所有代码基于 HarmonyOS 4.0 + Stage 模型 + ArkTS,已在 DevEco Studio 5.0 中验证通过。

Logo

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

更多推荐