前言

“我的”页最容易做成一个只展示信息的页面:

  • 能看到姓名、岗位、班次
  • 能换头像
  • 能退出登录

但只要没有修改密码,这套账号体系就还差最后一段闭环。

这次 注塑工程师助手 已经把这条链路完整接通了:

  1. “我的”页新增“修改密码”入口
  2. 调用真实接口 PUT /auth/password
  3. 前端做基础密码强度校验
  4. 页面实时显示“弱 / 合格 / 强”强度条
  5. 页面实时显示“确认新密码是否一致”
  6. 修改成功后弹出确认窗口
  7. 用户确认后立即退出登录并回到登录页
  8. 登录页提示使用新密码重新登录,并回填原账号

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

在这里插入图片描述

在这里插入图片描述

一、先确认后端接口:PUT /auth/password

这条链路基于现有后端真实接口,不是前端假表单。

后端接口定义如下:

@PutMapping("/auth/password")
public ApiResult<?> updatePassword(@RequestBody UpdatePasswordParam param) {
    if (StrUtil.hasBlank(param.getOldPassword(), param.getPassword())) {
        return fail("参数不能为空");
    }
    Integer userId = getLoginUserId();
    if (userId == null) {
        return fail("未登录");
    }
    if (!userService.comparePassword(userService.getById(userId).getPassword(), param.getOldPassword())) {
        return fail("原密码输入不正确");
    }
    User user = new User();
    user.setUserId(userId);
    user.setPassword(userService.encodePassword(param.getPassword()));
    if (userService.updateById(user)) {
        return success("修改成功");
    }
    return fail("修改失败");
}

前端请求体只需要两个字段:

{
  "oldPassword": "旧密码",
  "password": "新密码"
}

这里没有额外的验证码、短信码或二次确认参数,所以前端的重点不在接口复杂度,而在完整的页面反馈和登录态收口。

二、修改密码成功后,旧登录态立即失效

这次实现没有停在“提交成功弹个 toast”。

当前工程的规则是:

密码修改成功后,当前登录态立即失效,用户确认提示后回到登录页,再用新密码重新登录。

这样做的结果很明确:

  1. 密码变更和会话失效是一个连续动作
  2. 用户不会在“密码已修改”的前提下继续停留在旧业务页
  3. 登录页会明确告诉用户为什么回来了

这条规则由 AuthRepository 收口,页面层只负责收集输入和展示状态。

三、接口路径放在配置层,调用留在 AuthApi

当前工程把接口路径单独放在了 AppApiConfig.ets

export const APP_API_AUTH_PASSWORD_PATH: string = '/api/auth/password';

AuthApi.ets 里的实现保持得很干净:

class ChangePasswordRequestPayload {
  oldPassword: string;
  password: string;

  constructor(oldPassword: string, password: string) {
    this.oldPassword = oldPassword;
    this.password = password;
  }
}

async changePassword(oldPassword: string, password: string): Promise<void> {
  const requestPayload: ChangePasswordRequestPayload = new ChangePasswordRequestPayload(oldPassword, password);
  await this.putData(APP_API_AUTH_PASSWORD_PATH, requestPayload);
}

这里接口层只做两件事:

  1. 组装请求体
  2. 发起 PUT 请求

至于密码规则、错误提示、成功后的退出登录,都没有放进 AuthApi

四、业务收口放在 AuthRepository

真正的核心逻辑集中在 AuthRepository.ets

当前实现如下:

async changePassword(oldPassword: string, nextPassword: string, confirmPassword: string): Promise<string> {
  const normalizedOldPassword: string = oldPassword.trim();
  const normalizedNextPassword: string = nextPassword.trim();
  const normalizedConfirmPassword: string = confirmPassword.trim();
  if (normalizedOldPassword.length === 0) return '请输入当前密码。';

  const passwordRuleMessage: string = validateBasicPasswordRule(normalizedNextPassword);
  if (passwordRuleMessage.length > 0) return passwordRuleMessage;

  if (normalizedConfirmPassword.length === 0) return '请再次输入新密码。';
  if (normalizedNextPassword !== normalizedConfirmPassword) return '两次输入的新密码不一致。';
  if (normalizedOldPassword === normalizedNextPassword) return '新密码不能与当前密码相同。';
  if (!this.isLoggedIn()) return '当前登录状态已失效,请重新登录。';
  try {
    await authApi.changePassword(normalizedOldPassword, normalizedNextPassword);
    AppStorage.setOrCreate('authLoginNotice', '密码已修改,请使用新密码重新登录。');
    AppStorage.setOrCreate('authLoginHintUserId', this.currentUserId());
    return '';
  } catch (error) {
    return this.changePasswordErrorMessage(error);
  }
}

这段代码把四个动作一次收住了:

  1. 当前密码校验
  2. 新密码规则校验
  3. 确认密码一致性校验
  4. 修改成功后的登录页提示和账号回填

注意这里没有直接 logout(),因为当前版本把“退出登录”放在成功确认弹窗之后执行,这样用户能先看到明确的成功说明。

五、密码强度规则已经固定为“基础强度”

当前工程使用的是一套明确的基础规则:

  • 至少 8 位
  • 必须包含字母
  • 必须包含数字
  • 不能和旧密码相同

这套规则没有散在页面里,而是提取成了一个单独工具文件 PasswordStrengthUtil.ets

基础校验函数如下:

export function validateBasicPasswordRule(password: string): string {
  const normalizedPassword: string = password.trim();
  if (normalizedPassword.length === 0) {
    return '请输入新密码。';
  }
  if (normalizedPassword.length < 8) {
    return '新密码至少需要 8 位。';
  }
  const containsLetter: boolean = hasLetter(normalizedPassword);
  const containsDigit: boolean = hasDigit(normalizedPassword);
  if (!containsLetter || !containsDigit) {
    return '新密码需同时包含字母和数字。';
  }
  return '';
}

这样一来,规则的定义和页面展示被拆开了:

  • Repository 负责提交前校验
  • Page 负责把强度结果显示给用户

六、密码强度不是只给一句文案,而是做成了完整视觉态

当前修改密码页已经不是简单地显示“密码太弱”。

它现在有一整套实时视觉反馈:

  1. 密码强度:弱 / 合格 / 强
  2. 3 段式颜色强度条
  3. 规则命中标签:
    • 8 位及以上
    • 包含字母
    • 包含数字

强度判定逻辑来自 passwordStrengthSnapshot()

export function passwordStrengthSnapshot(password: string): PasswordStrengthSnapshot {
  const normalizedPassword: string = password.trim();
  if (normalizedPassword.length === 0) {
    return new PasswordStrengthSnapshot('none', '', '');
  }
  const containsLetter: boolean = hasLetter(normalizedPassword);
  const containsDigit: boolean = hasDigit(normalizedPassword);
  if (normalizedPassword.length < 8 || !containsLetter || !containsDigit) {
    return new PasswordStrengthSnapshot('weak', '弱', '至少 8 位,并同时包含字母和数字');
  }
  const containsUppercase: boolean = hasUppercase(normalizedPassword);
  const containsLowercase: boolean = hasLowercase(normalizedPassword);
  const containsSymbol: boolean = hasSymbol(normalizedPassword);
  if ((containsUppercase && containsLowercase) || containsSymbol) {
    return new PasswordStrengthSnapshot('strong', '强', '当前密码强度较好');
  }
  return new PasswordStrengthSnapshot('medium', '合格', '已满足基础强度要求');
}

页面里对应的视觉态已经做成三段条:

Row({ space: 8 }) {
  Column()
    .layoutWeight(1)
    .height(8)
    .backgroundColor(this.strengthActiveCount() >= 1 ? this.strengthColor() : '#E5EAF2')
    .borderRadius(999);
  Column()
    .layoutWeight(1)
    .height(8)
    .backgroundColor(this.strengthActiveCount() >= 2 ? this.strengthColor() : '#E5EAF2')
    .borderRadius(999);
  Column()
    .layoutWeight(1)
    .height(8)
    .backgroundColor(this.strengthActiveCount() >= 3 ? this.strengthColor() : '#E5EAF2')
    .borderRadius(999);
}

当前视觉规则已经固定:

  • :橙色,亮 1 段
  • 合格:蓝色,亮 2 段
  • :绿色,亮 3 段

七、规则命中状态也做成了标签,而不是藏在提示文字里

除了强度条,这次还把规则命中做成了胶囊标签。

对应的数据结构如下:

export class PasswordRequirementSnapshot {
  minLengthMet: boolean;
  letterMet: boolean;
  digitMet: boolean;
}

生成命中状态:

export function passwordRequirementSnapshot(password: string): PasswordRequirementSnapshot {
  const normalizedPassword: string = password.trim();
  return new PasswordRequirementSnapshot(
    normalizedPassword.length >= 8,
    hasLetter(normalizedPassword),
    hasDigit(normalizedPassword)
  );
}

页面里每个标签都是独立渲染的:

this.requirementChip('8 位及以上', this.requirementSnapshot().minLengthMet)
this.requirementChip('包含字母', this.requirementSnapshot().letterMet)
this.requirementChip('包含数字', this.requirementSnapshot().digitMet)

这样用户输入时能立刻看见是哪一条规则还没满足,不需要自己猜测。

八、“确认新密码”也有实时一致性反馈

当前版本不只是校验提交时的一致性,确认密码输入过程中也有实时反馈。

页面逻辑如下:

private confirmStatusLabel(): string {
  const normalizedNextPassword: string = this.nextPassword.trim();
  const normalizedConfirmPassword: string = this.confirmPassword.trim();
  if (normalizedConfirmPassword.length === 0) {
    return '';
  }
  if (normalizedNextPassword.length === 0) {
    return '请先输入新密码';
  }
  if (normalizedNextPassword === normalizedConfirmPassword) {
    return '两次输入一致';
  }
  return '两次输入不一致';
}

页面展示规则已经固定为:

  • 一致:绿色 ✓ 两次输入一致
  • 不一致:橙色 ! 两次输入不一致
  • 还没输入新密码:提示 请先输入新密码

这一层实时反馈把“提交流程中的报错”前移成了“输入过程中的自检”。

九、修改成功后不是直接跳走,而是先弹确认窗口

当前版本已经取消了“提交成功后 260ms 直接跳回登录页”的做法,改成了更明确的确认流程。

修改成功后先拉起一个确认弹层:

if (this.successDialogVisible) {
  Column({ space: 16 }) {
    Text('密码修改成功')
    Text('为了账号安全,当前登录状态将立即失效。请点击下方按钮,返回登录页后使用新密码重新登录。')
    Button('我知道了,返回登录')
      .onClick(() => void this.confirmSuccess());
  }
}

确认按钮点击后再真正退出登录:

private async confirmSuccess(): Promise<void> {
  this.successDialogVisible = false;
  await authRepository.logout();
  this.onDone();
}

这样用户能明确知道发生了什么,也能自己完成最后一步确认。

十、登录页会承接改密后的回流提示

这条链路不是“退出就结束”,登录页已经接住了后续提示。

修改成功后,Repository 写入两个状态:

AppStorage.setOrCreate('authLoginNotice', '密码已修改,请使用新密码重新登录。');
AppStorage.setOrCreate('authLoginHintUserId', this.currentUserId());

登录页读取后会完成两件事:

  1. 显示绿色提示条
  2. 自动回填原账号

登录页回填逻辑如下:

aboutToAppear(): void {
  if (this.userId.trim().length === 0 && this.authLoginHintUserId.trim().length > 0) {
    this.userId = this.authLoginHintUserId.trim();
  }
}

这样用户回到登录页时不需要重新输入账号,只需要输入新密码。

十一、“我的”页入口已经和当前界面风格统一

这次没有在个人中心额外加一排零碎按钮,而是沿用了当前“快捷设置”卡片样式。

入口代码如下:

ProfileQuickTile({
  token: '密',
  title: '修改密码',
  description: '修改后需要重新登录',
  onPress: () => this.onOpenPasswordEditor()
});

这个入口已经固定在“我的”页快捷设置区,和“头像与资料”“版本信息”“退出登录”处于同一组交互层级。

十二、页面切换继续交给 Index.ets

当前工程没有为了修改密码单独引入新路由机制,仍然沿用现有页面分支方式。

Index.ets 中使用:

@State profilePasswordEditing: boolean = false;

并在主内容区域里切换:

} else if (this.profilePasswordEditing) {
  ProfilePasswordPage({
    onCancel: () => { this.profilePasswordEditing = false; },
    onDone: () => {
      this.profilePasswordEditing = false;
      this.selectedTab = this.navigationStore.select('workbench');
      this.closeDetail();
    }
  }).layoutWeight(1)
}

这样修改密码页的组织方式和头像裁剪页保持了一致,整个工程结构没有被打散。

十三、当前实现已经覆盖的真实体验

截至这次整理,修改密码链路已经具备以下实际体验:

  1. 入口清晰,位于“我的”页快捷设置区
  2. 表单字段完整:当前密码、新密码、确认新密码
  3. 新密码实时强度显示
  4. 新密码规则命中标签实时刷新
  5. 确认密码一致性实时提示
  6. 提交失败文案有统一兜底
  7. 修改成功后先弹确认窗口
  8. 用户确认后退出登录
  9. 登录页展示改密成功提示
  10. 登录页回填原账号

这套实现已经不是一个“有接口的表单”,而是完整的账号安全闭环。

十四、总结

这次修改密码能力的改造,已经完整落成四层结构:

  1. AppApiConfig 负责接口路径配置
  2. AuthApi 负责修改密码接口调用
  3. AuthRepository 负责规则校验、错误处理和登录态收口
  4. ProfilePasswordPage 负责输入、强度条、规则标签、一致性提示和成功确认窗口

附录:工程配置与版本说明

为了便于读者复现本文中的代码片段和运行现象,这里把当前文章系列对应的工程基线单独列出。本文所说的“当前工程”,指 e_notebook 项目的 HarmonyOS ArkTS 客户端,应用名称为“注塑工程师助手”,主要用于脱敏演示机台档案、产品档案、调机记录、参数模板、异常闭环、生产批次和看板报表等业务路径。

1. 应用与模块配置

  • 应用包名:com.atan.enotebook
  • 应用版本:versionName1.0.0versionCode1000000
  • 工程模型:ArkTS / ArkUI Stage 模型。
  • 主模块:entry,模块类型为 entry
  • 入口 Ability:EntryAbility,入口文件为 entry/src/main/ets/entryability/EntryAbility.ets
  • 主页面配置:模块通过 pages: "$profile:main_pages" 读取页面列表。
  • 设备类型:当前模块声明支持 phonetablet2in1
  • 安装方式:deliveryWithInstalltrueinstallationFreefalse,属于随应用安装的普通 entry 模块。

2. SDK 与 API 版本

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

  • DevEco Studio 版本:DevEco Studio Beta 26.0.0.461
  • 编译 SDK:HarmonyOS SDK API 26 Beta1,SDK 包版本为 26.0.0.23
  • SDK 平台信息:apiVersion26platformVersion26.0.0releaseType / stageBeta1
  • targetSdkVersion26.0.0
  • compatibleSdkVersion6.1.1(24)
  • API 口径说明:文章系列以 API 24 作为兼容目标进行表述;当前工程实际由 API 26 Beta SDK 编译,并在 API 24 模拟器上做过安装、启动和交互观察。因此,文中的“API 24 运行观察”表示兼容目标环境下的模拟器验证结果,不等同于使用 API 24 SDK 重新完成编译验证。

3. 构建与运行工具

  • 开发工具 IDE:DevEco Studio Beta,安装目录指向 D:/Program Files/Huawei/DevEco Studio Beta
  • SDK 路径:D:/Program Files/Huawei/DevEco Studio Beta/sdk
  • 构建系统:Hvigor,工程入口 hvigorfile.ts 使用 @ohos/hvigor-ohos-pluginappTasks
  • Hvigor 执行配置:开启 daemon、incremental、parallel 和 typeCheck,日志级别为 info
  • 构建脚本:本地 build.ps1 优先使用 DevEco Studio 自带的 JBR、Node.js、SDK 与 Hvigor,避免系统环境变量中的 Java 或 Node.js 版本干扰构建结果。
  • 调试产物:未配置签名时,本地构建生成 entry/build/default/outputs/default/entry-default-unsigned.hap。这类 unsigned HAP 只用于本地调试和模拟器验证,正式发布前需要在 DevEco Studio 中补充签名配置。

4. 本系列文章的验证边界

  • 本系列代码以脱敏演示数据为主,Repository、Store、页面状态和组件边界都围绕本地演示闭环展开。
  • 已观察过的运行现象以文中对应截图、布局树和人工核对记录为准;没有重新核对的页面,不在单篇文章中扩大为完整结论。
  • 如果读者使用更新的 DevEco Studio、HarmonyOS SDK 或真机系统版本复现,API 差异、控件行为和签名流程可能会发生变化。遇到差异时,建议优先核对 build-profile.json5module.json5、SDK Manager 中安装的 API 版本,以及当前设备或模拟器的系统 API 等级。

附录 2:项目目录结构与设计意图

下面这份目录说明对应当前 DevEco Studio 中打开的 harmonyos-app 工程。截图里能看到的目录并不只是文件摆放习惯,它反映了一个 ArkTS Stage 工程的分层方式:应用级配置、业务模块、页面源码、资源文件、构建配置和过程归档分别放在不同位置,方便后续排查问题时先判断“问题属于配置、页面、数据、状态、资源,还是构建产物”。
在这里插入图片描述

harmonyos-app/
├── AppScope/                         # 应用级配置与全局资源入口
│   ├── app.json5                     # bundleName、版本号、图标、应用标签等应用级元信息
│   └── resources/                    # 应用级图标、字符串和基础资源
├── entry/                            # 主业务模块,当前 App 的主要页面和业务代码都在这里
│   ├── src/main/ets/                 # ArkTS 源码根目录
│   │   ├── components/               # 可复用 ArkUI 组件,如底部导航、数据状态面板
│   │   ├── entryability/             # Stage 模型入口 Ability,负责应用启动入口
│   │   ├── features/                 # 按业务域拆分的功能页面
│   │   │   ├── debug/                # 调机记录相关页面
│   │   │   ├── exceptions/           # 异常处置与闭环相关页面
│   │   │   ├── home/                 # 首页看板与概览入口
│   │   │   ├── machines/             # 机台档案列表、详情和机台相关交互
│   │   │   ├── production/           # 生产批次、报工和结案门禁相关页面
│   │   │   ├── products/             # 产品档案、产品详情和关联信息
│   │   │   ├── reports/              # 周报、月报、班次报表和下钻入口
│   │   │   └── templates/            # 参数模板列表与详情
│   │   ├── models/                   # 业务对象的数据结构,如 Machine、Product、DebugRecord
│   │   ├── pages/                    # 页面容器与导航装配,如 Index.ets
│   │   ├── repositories/             # 脱敏演示数据、查询方法、快照持久化和数据重置边界
│   │   ├── stores/                   # 页面路由、导航选择和共享状态规则
│   │   └── utils/                    # 主题令牌、校验函数等通用工具
│   ├── src/main/resources/base/      # 模块级资源目录
│   │   ├── element/                  # 字符串、颜色等基础资源声明
│   │   ├── media/                    # 图标、启动图等媒体资源
│   │   └── profile/                  # 页面 profile 配置,如 main_pages.json
│   ├── src/main/module.json5         # entry 模块配置,声明 EntryAbility、设备类型和页面入口
│   ├── build-profile.json5           # 模块级构建目标、混淆和 target 配置
│   └── oh-package.json5              # entry 模块包信息与依赖声明
├── hvigor/                           # Hvigor 构建系统配置
│   └── hvigor-config.json5           # 构建执行参数,如增量、并行和类型检查
├── build-profile.json5               # 工程级 SDK、targetSdkVersion、compatibleSdkVersion 配置
├── hvigorfile.ts                     # 工程级构建任务入口,接入 appTasks
├── local.properties                  # 本机 SDK 路径配置
├── oh-package.json5                  # 工程级包信息与依赖声明
├── build.ps1                         # 本地构建脚本,固定使用 DevEco Studio 自带工具链
├── document_claude/                  # 开发过程归档、测试记录和验证材料
├── .hvigor/                          # Hvigor 生成的缓存和构建记录,不作为手写源码维护
├── .idea/                            # DevEco Studio / IntelliJ 工程配置,不承载业务逻辑
└── entry/build/                      # 构建输出目录,HAP 和中间产物由构建流程生成

1. 为什么应用级配置放在 AppScope

AppScope 负责应用整体身份,而不是某个页面的业务逻辑。app.json5 中的 bundleNameversionNameversionCode、应用图标和应用标签,会影响安装包身份、桌面展示和版本识别。把这类配置放在应用级目录,可以避免业务页面为了改一个标题或图标而混入应用发布配置。

在当前工程中,AppScope 更像“应用身份证”。它回答的是“这个 App 是谁、版本是多少、展示什么图标”,而不是“机台列表怎么筛选、详情页怎么返回”。

2. 为什么业务代码集中在 entry/src/main/ets

entry 是当前工程的主业务模块,src/main/ets 是 ArkTS 源码根目录。截图里打开的 MachineDetail.ets 就位于 features/machines 下面,说明机台详情页被归入“机台业务域”,而不是随意放在全局页面目录中。

这种组织方式的好处是定位明确:机台问题优先看 features/machines,产品问题优先看 features/products,生产批次问题优先看 features/production。当文章里讨论某个业务链路时,读者也能从目录直接反推代码位置。

3. componentsfeaturespages 的边界

components 放的是可复用组件,例如底部导航、加载/空态/失败态面板。它们不应该直接知道“当前打开的是哪台机台”,而是通过参数和回调服务于不同页面。

features 放的是业务域页面。每个子目录都围绕一个业务主题组织,例如 machines 负责机台档案,templates 负责参数模板,exceptions 负责异常闭环。业务页面可以组合组件,也可以读取模型和仓储,但应尽量把本业务域的显示和交互留在本目录内。

pages 更偏页面容器和入口装配。当前 Index.ets 承担主页面状态切换、底部导航和详情路径分发等职责。它不应该塞满所有业务细节,而是负责把用户当前所在位置、打开对象和页面分支组织起来。

4. modelsrepositoriesstores 分别解决什么问题

models 定义数据形状,例如机台、产品、调机记录、生产批次等对象有哪些字段。它让页面和仓储使用同一套类型语言,避免每个页面临时拼对象。

repositories 定义数据来源和查询边界。当前工程使用脱敏演示数据和本地持久化快照,因此仓储层负责“从哪里取数据、按什么 ID 查询、怎样重置演示数据”。页面不直接关心数据是内置数组、Preferences 快照,还是后续真实接口。

stores 定义页面级或应用级状态规则,例如当前导航项、路由分支、打开详情的类型和 ID。把状态规则从具体组件中抽出来,可以减少“列表、详情、导航互相覆盖状态”的问题。

5. 为什么资源放在 resources/base

resources/base/element 管字符串、颜色等声明,resources/base/media 管图标和图片,resources/base/profile 管页面 profile。它们和 ArkTS 页面代码分开,是为了让“界面逻辑”和“静态资源”各自清晰。

如果页面显示异常,先判断是布局代码问题还是资源引用问题。比如图标不显示,应优先检查 media 和资源引用;页面无法进入,应检查 profile/main_pages.jsonmodule.json5 的页面声明;颜色或字符串不符合预期,则回到 element 下核对。

6. 构建目录和生成目录不要手工维护

.hvigorentry/build 和部分中间产物目录由构建系统生成,主要用于缓存、编译记录、HAP 输出和临时文件。它们可以帮助排查构建结果,但不应该作为手写业务代码维护。

当前调试 HAP 位于 entry/build/default/outputs/default/entry-default-unsigned.hap。这个路径说明构建已经产出安装包,但它仍是 unsigned 调试产物;正式发布前应回到 DevEco Studio 的签名配置和发布流程,而不是直接修改 build 目录里的文件。

在这里插入图片描述

Logo

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

更多推荐