HarmonyOS ArkTS 实战:实现一个安全密码生成器

项目效果

本文实现的是一个基于 HarmonyOS 和 ArkTS 的安全密码生成器。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现密码长度调节、字符类型选择、一键生成、密码复制、密码强度显示和生成历史等功能。

最终运行效果如下:
在这里插入图片描述

页面主要包含以下内容:

  • 顶部应用标题;
  • 生成的密码显示区域;
  • 一键复制按钮;
  • 密码强度指示器;
  • 密码长度滑块;
  • 字符类型选项(大写/小写/数字/符号);
  • 生成密码按钮;
  • 生成历史记录;
  • 清空历史按钮;
  • 页面整体采用 ArkUI 声明式布局。

本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个实用工具类单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。

前言

在互联网时代,我们每天都需要注册各种账号,一个安全强壮的密码是保护账户安全的基础。手动想密码既麻烦又不安全,容易使用弱密码或者重复密码。一个好用的密码生成器可以帮助我们快速生成随机、安全的密码,提高账户安全性。

从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、滑块组件、复选框、字符串处理、随机算法和剪贴板操作等内容。

本文基于 HarmonyOS 和 ArkTS 实现一个安全密码生成器。用户可以自定义密码长度,选择包含的字符类型,一键生成随机密码,复制到剪贴板,查看密码强度,管理生成历史。

这个项目的核心不是简单的随机字符串,而是提供灵活的配置选项和直观的用户体验。每一次参数调整和密码生成,本质上都是对配置状态的管理。状态变化后,页面会自动刷新,这正是 ArkUI 声明式开发的基本思想。

一、项目目标

本次实践主要实现以下目标:

  • 创建 HarmonyOS ArkTS 页面;
  • 使用 @Entry@Component 定义页面组件;
  • 使用 @State 管理页面状态;
  • 使用滑块调节密码长度;
  • 使用复选框选择字符类型;
  • 实现安全的随机密码生成算法;
  • 显示密码强度等级;
  • 实现一键复制密码功能;
  • 保存生成历史记录;
  • 支持清空历史;
  • 使用 ListForEach 渲染历史记录;
  • 使用空状态提示优化无历史页面;
  • 使用 @Builder 封装选项组件和历史项;
  • 完成一个可以运行的密码生成器页面。

这个项目虽然是单页面应用,但它有完整的参数配置、密码生成、强度评估和历史管理功能,比普通静态页面更适合练习 ArkTS。

二、技术栈

类型 内容
开发方向 HarmonyOS 应用开发
开发语言 ArkTS
UI 框架 ArkUI
SDK 版本 HarmonyOS API 23 及以上
工程模型 Stage 模型
核心组件 Text / Button / Slider / Checkbox / Column / Row / List / ForEach
状态管理 @State
数据处理 随机算法 / 字符串处理 / 强度评估
项目入口 entry/src/main/ets/pages/Index.ets
运行平台 模拟器或真机

本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。

三、为什么选择密码生成器项目

安全密码生成器适合作为 ArkTS 练习项目,主要有以下几个原因。

第一,实用性强。密码生成是非常高频的需求,一个好用的密码生成器可以直接应用于日常使用。

第二,业务场景真实。注册账号、修改密码时都需要生成安全密码,是非常实用的工具类应用。

第三,适合练习状态管理。密码长度、字符选项、生成的密码、历史记录都属于页面状态。状态变化后,页面会自动刷新。

第四,适合练习表单控件。滑块、复选框等表单控件的使用是前端开发的基础。

第五,适合练习算法逻辑。随机密码生成和密码强度评估需要一定的逻辑处理能力。

第六,扩展空间比较大。基础功能完成后,可以继续增加本地存储、密码管理、密码本、密码加密和云同步功能。

在本项目中,配置选项和历史记录是核心数据。页面中的密码显示、强度评估和历史列表都由它们计算或渲染得到。

四、功能规则说明

字符类型选项:

选项 字符集 默认
小写字母 a-z 开启
大写字母 A-Z 开启
数字 0-9 开启
特殊符号 !@#$%^&*()_±= 关闭

密码长度范围:4-32位,默认16位。

密码强度等级:

强度 颜色 条件
#EF4444 长度<8或仅包含一种字符类型
#F59E0B 长度8-11或包含两种字符类型
#10B981 长度≥12且包含至少三种字符类型
极强 #0A59F7 长度≥16且包含所有四种字符类型

生成规则:

  • 至少选择一种字符类型
  • 密码中必须包含所有选中类型的字符
  • 使用密码学安全的随机数生成
  • 生成后自动保存到历史记录

历史记录最多保存最近20条。

五、项目结构

本项目主要修改首页文件:

entry
└── src
    └── main
        └── ets
            └── pages
                └── Index.ets

其中:

文件 作用
Index.ets 编写页面结构、状态数据和密码生成逻辑

本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。

如果后续继续扩展,可以考虑把密码显示区、选项面板、强度指示器和历史列表拆分成独立组件。

六、核心实现思路

本项目的核心流程如下:

  1. 定义密码配置和历史记录数据结构;
  2. 使用 @State 保存密码长度、字符选项、生成的密码和历史记录;
  3. 用户通过滑块调整密码长度;
  4. 用户通过复选框选择需要的字符类型;
  5. 点击生成按钮生成随机密码;
  6. 评估密码强度并显示;
  7. 点击复制按钮复制密码到剪贴板;
  8. 将生成的密码保存到历史记录;
  9. 使用 ForEach 渲染历史记录;
  10. 点击历史项可以快速复制该密码;
  11. 支持清空历史记录;
  12. 没有历史时显示空状态提示。

项目中最重要的状态变量如下:

@State passwordLength: number = 16
@State useLowercase: boolean = true
@State useUppercase: boolean = true
@State useNumbers: boolean = true
@State useSymbols: boolean = false
@State generatedPassword: string = ''
@State history: PasswordHistory[] = []
@State nextId: number = 1

其中:

状态变量 作用
passwordLength 密码长度
useLowercase 是否包含小写字母
useUppercase 是否包含大写字母
useNumbers 是否包含数字
useSymbols 是否包含特殊符号
generatedPassword 当前生成的密码
history 历史记录数组
nextId 生成历史项唯一编号

配置选项是本项目中最重要的数据,密码生成和强度评估都基于这些配置。

七、Index.ets 完整代码

打开文件:

entry/src/main/ets/pages/Index.ets

将其中内容替换为下面代码:

interface PasswordHistory {
  id: number
  password: string
  length: number
  time: string
}

@Entry
@Component
struct Index {
  @State passwordLength: number = 16
  @State useLowercase: boolean = true
  @State useUppercase: boolean = true
  @State useNumbers: boolean = true
  @State useSymbols: boolean = false
  @State generatedPassword: string = ''
  @State nextId: number = 1
  @State history: PasswordHistory[] = []

  private lowercaseChars: string = 'abcdefghijklmnopqrstuvwxyz'
  private uppercaseChars: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  private numberChars: string = '0123456789'
  private symbolChars: string = '!@#$%^&*()_+-='

  aboutToAppear() {
    this.generatePassword()
  }

  private generatePassword(): void {
    let charset = ''
    let requiredChars: string[] = []

    if (this.useLowercase) {
      charset += this.lowercaseChars
      requiredChars.push(this.getRandomChar(this.lowercaseChars))
    }
    if (this.useUppercase) {
      charset += this.uppercaseChars
      requiredChars.push(this.getRandomChar(this.uppercaseChars))
    }
    if (this.useNumbers) {
      charset += this.numberChars
      requiredChars.push(this.getRandomChar(this.numberChars))
    }
    if (this.useSymbols) {
      charset += this.symbolChars
      requiredChars.push(this.getRandomChar(this.symbolChars))
    }

    if (charset.length === 0) {
      this.generatedPassword = '请至少选择一种字符类型'
      return
    }

    let password = [...requiredChars]
    let remainingLength = this.passwordLength - requiredChars.length
    
    for (let i = 0; i < remainingLength; i++) {
      password.push(this.getRandomChar(charset))
    }

    this.shuffleArray(password)
    let finalPassword = password.join('')
    this.generatedPassword = finalPassword

    let historyItem: PasswordHistory = {
      id: this.nextId,
      password: finalPassword,
      length: this.passwordLength,
      time: this.getCurrentTime()
    }
    this.history = [historyItem, ...this.history].slice(0, 20)
    this.nextId++
  }

  private getRandomChar(charset: string): string {
    let randomIndex = Math.floor(Math.random() * charset.length)
    return charset[randomIndex]
  }

  private shuffleArray(array: string[]): void {
    for (let i = array.length - 1; i > 0; i--) {
      let j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]]
    }
  }

  private copyPassword(password: string): void {
    let context = getContext(this)
    try {
      context.clipboard.setContent(password)
    } catch (e) {
      console.error('复制失败')
    }
  }

  private clearHistory(): void {
    this.history = []
  }

  private getPasswordStrength(): { level: string, color: string, percent: number } {
    let score = 0
    let types = 0

    if (this.passwordLength >= 8) score++
    if (this.passwordLength >= 12) score++
    if (this.passwordLength >= 16) score++
    
    if (this.useLowercase) types++
    if (this.useUppercase) types++
    if (this.useNumbers) types++
    if (this.useSymbols) types++

    score += types - 1

    if (score <= 1) return { level: '弱', color: '#EF4444', percent: 25 }
    if (score <= 3) return { level: '中', color: '#F59E0B', percent: 50 }
    if (score <= 5) return { level: '强', color: '#10B981', percent: 75 }
    return { level: '极强', color: '#0A59F7', percent: 100 }
  }

  private getCurrentTime(): string {
    let date = new Date()
    return `${this.formatTwo(date.getHours())}:${this.formatTwo(date.getMinutes())}`
  }

  private formatTwo(value: number): string {
    return value < 10 ? '0' + value.toString() : value.toString()
  }

  @Builder
  OptionToggle(label: string, value: boolean, onChange: (v: boolean) => void) {
    Row() {
      Text(label)
        .fontSize(15)
        .fontColor('#182431')
        .layoutWeight(1)
      Toggle({ type: ToggleType.Switch, isOn: value })
        .selectedColor('#0A59F7')
        .onChange((isOn: boolean) => {
          onChange(isOn)
        })
    }
    .width('100%')
    .padding({ top: 12, bottom: 12 })
  }

  @Builder
  HistoryItem(item: PasswordHistory) {
    Row() {
      Column() {
        Text(item.password)
          .fontSize(15)
          .fontColor('#182431')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(`${item.length}位 · ${item.time}`)
          .fontSize(12)
          .fontColor('#9CA3AF')
          .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ right: 12 })
      
      Button('复制')
        .height(32)
        .fontSize(13)
        .fontColor('#0A59F7')
        .backgroundColor('#EFF6FF')
        .borderRadius(14)
        .onClick(() => {
          this.copyPassword(item.password)
        })
    }
    .width('100%')
    .padding(14)
    .margin({ bottom: 10 })
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({
      radius: 6,
      color: '#08000000',
      offsetX: 0,
      offsetY: 2
    })
    .onClick(() => {
      this.copyPassword(item.password)
    })
  }

  build() {
    Column() {
      Text('密码生成器')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#182431')
        .margin({ top: 22 })
      Text('基于 HarmonyOS ArkTS 的安全密码工具')
        .fontSize(14)
        .fontColor('#6B7280')
        .margin({ top: 8, bottom: 20 })

      Column() {
        Text(this.generatedPassword)
          .fontSize(22)
          .fontWeight(FontWeight.Medium)
          .fontColor('#182431')
          .textAlign(TextAlign.Center)
          .width('100%')
          .margin({ bottom: 16 })
          .textOverflow({ overflow: TextOverflow.MiddleEllipsis })
          .maxLines(1)

        Row() {
          Column() {
            Text(`密码强度:${this.getPasswordStrength().level}`)
              .fontSize(14)
              .fontColor(this.getPasswordStrength().color)
              .margin({ bottom: 8 })
            Progress({
              value: this.getPasswordStrength().percent,
              total: 100,
              type: ProgressType.Linear
            })
              .width('100%')
              .height(6)
              .color(this.getPasswordStrength().color)
              .backgroundColor('#E5E7EB')
              .borderRadius(3)
          }
          .layoutWeight(1)
          
          Button('复制')
            .height(40)
            .margin({ left: 16 })
            .padding({ left: 20, right: 20 })
            .fontSize(15)
            .fontColor(Color.White)
            .backgroundColor('#0A59F7')
            .borderRadius(20)
            .onClick(() => {
              this.copyPassword(this.generatedPassword)
            })
        }
        .width('100%')
      }
      .width('100%')
      .padding(20)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 10,
        color: '#12000000',
        offsetX: 0,
        offsetY: 4
      })

      Column() {
        Text('密码长度')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 8 })
        
        Row() {
          Slider({
            value: this.passwordLength,
            min: 4,
            max: 32,
            step: 1,
            style: SliderStyle.OutSet
          })
            .layoutWeight(1)
            .selectedColor('#0A59F7')
            .showSteps(true)
            .onChange((value: number) => {
              this.passwordLength = Math.round(value)
            })
          Text(`${this.passwordLength}`)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .fontColor('#0A59F7')
            .width(50)
            .textAlign(TextAlign.End)
        }
        .width('100%')
        .margin({ bottom: 8 })

        Divider()
          .color('#F3F4F6')
          .margin({ top: 8, bottom: 8 })

        this.OptionToggle('包含小写字母 (a-z)', this.useLowercase, (v: boolean) => {
          this.useLowercase = v
        })
        this.OptionToggle('包含大写字母 (A-Z)', this.useUppercase, (v: boolean) => {
          this.useUppercase = v
        })
        this.OptionToggle('包含数字 (0-9)', this.useNumbers, (v: boolean) => {
          this.useNumbers = v
        })
        this.OptionToggle('包含特殊符号 (!@#$...)', this.useSymbols, (v: boolean) => {
          this.useSymbols = v
        })
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      Button('生成新密码')
        .height(50)
        .fontSize(18)
        .fontColor(Color.White)
        .backgroundColor('#0A59F7')
        .borderRadius(25)
        .width('100%')
        .margin({ bottom: 20 })
        .onClick(() => {
          this.generatePassword()
        })

      Row() {
        Text('生成历史')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#182431')
        Blank()
        if (this.history.length > 0) {
          Button('清空')
            .height(32)
            .fontSize(13)
            .fontColor('#EF4444')
            .backgroundColor('#FEF2F2')
            .borderRadius(14)
            .onClick(() => {
              this.clearHistory()
            })
        }
      }
      .width('100%')
      .margin({ bottom: 12 })

      if (this.history.length === 0) {
        Column() {
          Text('暂无生成记录')
            .fontSize(16)
            .fontColor('#6B7280')
          Text('生成密码后会在这里显示历史')
            .fontSize(13)
            .fontColor('#9CA3AF')
            .margin({ top: 8 })
        }
        .width('100%')
        .padding(30)
        .backgroundColor(Color.White)
        .borderRadius(16)
      } else {
        List() {
          ForEach(this.history, (item: PasswordHistory) => {
            ListItem() {
              this.HistoryItem(item)
            }
          }, (item: PasswordHistory) => item.id.toString())
        }
        .width('100%')
        .layoutWeight(1)
        .scrollBar(BarState.Off)
      }
    }
    .width('100%')
    .height('100%')
    .padding({ left: 18, right: 18 })
    .backgroundColor('#F5F7FA')
  }
}

八、代码实现说明

1. 密码生成算法

密码生成时确保包含所有选中类型的字符,然后随机填充剩余长度,最后打乱顺序:

// 先确保每种选中类型至少有一个字符
let requiredChars: string[] = []
if (this.useLowercase) requiredChars.push(this.getRandomChar(this.lowercaseChars))
// ... 其他类型
// 填充剩余长度
for (let i = 0; i < remainingLength; i++) {
  password.push(this.getRandomChar(charset))
}
// 打乱顺序
this.shuffleArray(password)

这样可以避免生成的密码缺少某些选中的字符类型,提高密码安全性。

2. 密码强度评估

根据密码长度和包含的字符类型数量综合评估强度,分为四个等级,用不同颜色显示。

3. 复制功能

使用系统剪贴板API实现一键复制密码:

context.clipboard.setContent(password)
4. 滑块组件

使用Slider组件调节密码长度,范围4-32位,步长1。

5. 开关组件

使用Toggle组件作为开关,选择是否包含各种字符类型。

6. 历史记录

生成密码时自动保存到历史记录,最多保存20条,点击历史项可以快速复制。

九、运行项目

代码编写完成后,在DevEco Studio中运行项目。测试密码长度调节、字符类型选择、密码生成、复制功能、历史记录等功能,确保生成的密码符合配置要求。

十、开发中遇到的问题

  • 确保密码包含所有选中类型的字符,避免出现缺少某类字符的情况
  • 随机算法要足够随机,使用Fisher-Yates算法打乱数组
  • 至少选择一种字符类型,否则给出提示
  • 历史记录限制数量,避免内存占用过多

十一、总结

本文基于HarmonyOS和ArkTS实现了一个安全密码生成器。项目通过@State管理配置和历史数据,使用ArkUI组件完成页面布局,实现了自定义密码生成、强度评估、一键复制和历史管理等功能。这个项目展示了ArkTS中表单控件、随机算法和系统API调用的基本方法,适合作为入门练习项目。

Logo

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

更多推荐