验证码登录与忘记密码

鸿蒙原生开发手记:徒步迹 - 验证码登录与忘记密码

实现短信验证码登录和密码重置功能


一、前言

除了密码登录,短信验证码登录和忘记密码是移动 App 的标配功能。本文在已实现的登录页基础上,增加验证码登录和密码重置的能力。


二、验证码登录

2.1 页面实现

@Entry
@Component
struct CodeLoginPage {
  @State private phone: string = '';
  @State private code: string = '';
  @State private countdown: number = 0;
  @State private isLoading: boolean = false;
  @State private phoneError: string = '';
  @State private codeError: string = '';

  build() {
    Column() {
      // 标题
      Text('验证码登录')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .width('100%');

      Text('输入手机号获取验证码')
        .fontSize(14)
        .fontColor('#666')
        .width('100%')
        .margin({ top: 8 });

      // 手机号输入
      TextInput({ placeholder: '请输入手机号', text: this.phone })
        .width('100%').height(50)
        .backgroundColor('#F5F5F5').borderRadius(12).padding({ left: 16 })
        .type(InputType.PhoneNumber).maxLength(11)
        .margin({ top: 30 })
        .onChange((v) => { this.phone = v; this.phoneError = ''; });

      // 手机号校验提示
      if (this.phoneError) {
        Text(this.phoneError).fontSize(12).fontColor('#F44336').width('100%').margin({ top: 4 });
      }

      // 验证码输入
      Row() {
        TextInput({ placeholder: '请输入验证码', text: this.code })
          .layoutWeight(1)
          .backgroundColor(Color.Transparent)
          .type(InputType.Number).maxLength(6)
          .onChange((v) => { this.code = v; this.codeError = ''; });

        Button(this.countdown > 0 ? `${this.countdown}s后重发` : '获取验证码')
          .backgroundColor(this.countdown > 0 ? '#999' : '#4CAF50')
          .borderRadius(8).height(40).fontSize(14).fontColor(Color.White)
          .enabled(this.countdown <= 0 && this.phone.length === 11)
          .onClick(() => {
            if (this.countdown <= 0) this.sendCode();
          });
      }
      .width('100%').height(50)
      .backgroundColor('#F5F5F5').borderRadius(12).padding({ left: 16 })
      .margin({ top: 16 });

      // 验证码提示
      if (this.codeError) {
        Text(this.codeError).fontSize(12).fontColor('#F44336').width('100%').margin({ top: 4 });
      }

      // 登录按钮
      Button() {
        if (this.isLoading) {
          LoadingProgress().width(24).height(24).color(Color.White);
        } else {
          Text('登录').fontSize(16).fontColor(Color.White);
        }
      }
      .width('100%').height(50)
      .backgroundColor(this.code.length === 6 ? '#4CAF50' : '#CCC')
      .borderRadius(25)
      .margin({ top: 32 })
      .enabled(this.code.length === 6 && !this.isLoading)
      .onClick(() => this.handleCodeLogin());

      // 切换到密码登录
      Text('使用密码登录 >')
        .fontSize(14)
        .fontColor('#4CAF50')
        .margin({ top: 20 })
        .onClick(() => { router.back(); });
    }
    .width('100%').height('100%')
    .padding({ left: 24, right: 24 })
    .backgroundColor(Color.White);
  }

  sendCode(): void {
    // 模拟发送验证码
    this.countdown = 60;
    const interval = setInterval(() => {
      this.countdown--;
      if (this.countdown <= 0) clearInterval(interval);
    }, 1000);
    console.log(`验证码已发送到 ${this.phone}`);
  }

  handleCodeLogin(): void {
    this.isLoading = true;
    setTimeout(() => {
      this.isLoading = false;
      AppStorage.setOrCreate('isLogged', true);
      router.replaceUrl({ url: 'pages/HomePage' });
    }, 1000);
  }
}

2.2 验证码后端接口(参考)

// 发送验证码
async function sendSmsCode(phone: string): Promise<boolean> {
  const response = await apiService.request('/api/auth/send-code', 'POST', {
    phone: phone,
    type: 'login',  // login / reset
  });
  return response.code === 200;
}

// 校验验证码
async function verifyCode(phone: string, code: string): Promise<string> {
  const response = await apiService.request('/api/auth/verify-code', 'POST', {
    phone,
    code,
  });
  if (response.code === 200) {
    return response.data.token;
  }
  throw new Error('验证码错误');
}

三、忘记/重置密码

3.1 流程设计

输入手机号 → 发送验证码 → 验证身份 → 设置新密码 → 完成

3.2 实现

@Entry
@Component
struct ResetPasswordPage {
  @State private step: number = 1;  // 1-验证身份 2-设置密码 3-完成
  @State private phone: string = '';
  @State private code: string = '';
  @State private newPassword: string = '';
  @State private confirmPassword: string = '';
  @State private countdown: number = 0;
  @State private isLoading: boolean = false;

  build() {
    Column() {
      // 步骤指示器
      Row() {
        this.StepDot(1, '验证身份');
        Divider().width(40).height(2).color(this.step >= 2 ? '#4CAF50' : '#E0E0E0');
        this.StepDot(2, '设置密码');
        Divider().width(40).height(2).color(this.step >= 3 ? '#4CAF50' : '#E0E0E0');
        this.StepDot(3, '完成');
      }
      .width('100%')
      .padding({ top: 30, bottom: 40 })
      .justifyContent(FlexAlign.Center);

      // 步骤内容
      if (this.step === 1) {
        this.StepVerifyIdentity();
      } else if (this.step === 2) {
        this.StepSetPassword();
      } else {
        this.StepComplete();
      }
    }
    .width('100%').height('100%')
    .padding({ left: 24, right: 24 })
    .backgroundColor(Color.White);
  }

  @Builder
  StepDot(step: number, label: string) {
    Column() {
      Text(`${step}`)
        .width(28).height(28)
        .fontSize(14)
        .fontColor(Color.White)
        .backgroundColor(this.step >= step ? '#4CAF50' : '#E0E0E0')
        .borderRadius(14)
        .textAlign(TextAlign.Center);

      Text(label)
        .fontSize(12)
        .fontColor(this.step >= step ? '#4CAF50' : '#999')
        .margin({ top: 4 });
    }
    .alignItems(HorizontalAlign.Center);
  }

  @Builder
  StepVerifyIdentity() {
    Column() {
      Text('验证身份').fontSize(20).fontWeight(FontWeight.Bold).width('100%');
      Text('输入注册时使用的手机号').fontSize(14).fontColor('#666').width('100%').margin({ top: 6 });

      TextInput({ placeholder: '手机号', text: this.phone })
        .width('100%').height(50).backgroundColor('#F5F5F5').borderRadius(12)
        .padding({ left: 16 }).type(InputType.PhoneNumber)
        .margin({ top: 24 });

      Row() {
        TextInput({ placeholder: '验证码', text: this.code })
          .layoutWeight(1).backgroundColor(Color.Transparent)
          .type(InputType.Number).maxLength(6);
        Button(this.countdown > 0 ? `${this.countdown}s` : '获取验证码')
          .backgroundColor(this.countdown > 0 ? '#999' : '#4CAF50')
          .borderRadius(8).height(36).fontSize(13).fontColor(Color.White)
          .onClick(() => { if (this.countdown <= 0) this.sendCode(); });
      }
      .width('100%').height(50).backgroundColor('#F5F5F5').borderRadius(12)
      .padding({ left: 16 }).margin({ top: 12 });

      Button('下一步').width('100%').height(50).backgroundColor('#4CAF50')
        .borderRadius(25).margin({ top: 30 })
        .enabled(this.phone.length === 11 && this.code.length === 6)
        .onClick(() => { this.step = 2; });
    }
    .width('100%');
  }

  @Builder
  StepSetPassword() {
    Column() {
      Text('设置新密码').fontSize(20).fontWeight(FontWeight.Bold).width('100%');

      TextInput({ placeholder: '新密码(至少6位)', text: this.newPassword })
        .width('100%').height(50).backgroundColor('#F5F5F5').borderRadius(12)
        .padding({ left: 16 }).type(InputType.Password).margin({ top: 24 });

      TextInput({ placeholder: '确认新密码', text: this.confirmPassword })
        .width('100%').height(50).backgroundColor('#F5F5F5').borderRadius(12)
        .padding({ left: 16 }).type(InputType.Password).margin({ top: 12 });

      Button('重置密码').width('100%').height(50).backgroundColor('#4CAF50')
        .borderRadius(25).margin({ top: 30 })
        .enabled(this.newPassword.length >= 6 && this.newPassword === this.confirmPassword)
        .onClick(() => { this.step = 3; });
    }
    .width('100%');
  }

  @Builder
  StepComplete() {
    Column() {
      Text('✅').fontSize(64);
      Text('密码重置成功').fontSize(20).fontWeight(FontWeight.Bold).margin({ top: 16 });
      Text('请使用新密码登录').fontSize(14).fontColor('#666').margin({ top: 8 });

      Button('返回登录').width('100%').height(50).backgroundColor('#4CAF50')
        .borderRadius(25).margin({ top: 40 })
        .onClick(() => { router.back(); });
    }
    .width('100%')
    .margin({ top: 60 })
    .alignItems(HorizontalAlign.Center);
  }

  sendCode(): void {
    this.countdown = 60;
    const interval = setInterval(() => {
      this.countdown--;
      if (this.countdown <= 0) clearInterval(interval);
    }, 1000);
  }
}

四、安全最佳实践

  1. 验证码有效期:通常 5 分钟
  2. 限制发送频率:同一手机号 60 秒内不可重复发送
  3. 加密传输:密码使用 HTTPS + 加密传输
  4. 前端脱敏:手机号中间 4 位隐藏 138****0000

五、总结

本文实现了验证码登录和密码重置两大功能,采用多步骤流程设计,用户体验清晰流畅。这些功能与密码登录共同组成了“徒步迹“完整的用户认证体系。

下一篇文章我们将回到首页,实现完整的首页布局。


下一篇预告:鸿蒙原生开发手记:徒步迹 - 首页布局设计与Scroll滚动

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐