reset-password.vue 9.33 KB
<template>
  <view class="login-wrapper">
    <!-- 主表单区域 -->
    <view class="form-container">
      <!-- 1. 手机号码 -->
      <view class="form-item-wrapper">
        <text class="form-label">手机号码</text>
        <view class="input-box">
          <u-input v-model="formData.phone" type="number" maxlength="11" placeholder="请输入您的手机号"
            placeholder-style="color: #a1a8b3" border="none" class="custom-u-input">
            <template #prefix>
              <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" />
            </template>
          </u-input>
        </view>
      </view>

      <!-- 2. 短信验证码 -->
      <view class="form-item-wrapper">
        <text class="form-label">验证码</text>
        <view class="code-row">
          <view class="input-box flex-1">
            <u-input v-model="formData.code" type="number" maxlength="6" placeholder="6位验证码"
              placeholder-style="color: #a1a8b3" border="none" class="custom-u-input">
              <template #prefix>
                <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" />
              </template>
            </u-input>
          </view>

          <!-- 获取验证码按钮 -->
          <button class="code-btn" :class="{ disabled: isCodeBtnDisabled }" :disabled="isCodeBtnDisabled"
            @click="handleSendCode">
            <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
          </button>
        </view>
      </view>

      <!-- 3. 设置新密码 -->
      <view class="form-item-wrapper">
        <text class="form-label">设置新密码</text>
        <view class="input-box">
          <u-input v-model="formData.password" type="password" placeholder="请设置您的新密码" placeholder-style="color: #a1a8b3"
            border="none" class="custom-u-input">
            <template #prefix>
              <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
            </template>
          </u-input>
        </view>
        <text class="input-hint">请设置8~16位包含数字、大小写字母、特殊字符组合作为密码</text>
      </view>

      <!-- 4. 确认新密码 -->
      <view class="form-item-wrapper">
        <text class="form-label">确认密码</text>
        <view class="input-box">
          <u-input v-model="formData.confirmPassword" type="password" placeholder="请再次输入新密码"
            placeholder-style="color: #a1a8b3" border="none" class="custom-u-input">
            <template #prefix>
              <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
            </template>
          </u-input>
        </view>
      </view>
    </view>

    <!-- 5. 提交按钮 -->
    <view class="submit-btn" :class="{ 'disabled-btn': isSubmitting }" @click="handleSubmit">
      <text>{{ isSubmitting ? '保存中...' : '确认修改' }}</text>
    </view>
  </view>
</template>

<script setup>
import { ref, reactive, computed, onBeforeUnmount } from 'vue';
import { onLoad, onHide, onUnload } from '@dcloudio/uni-app';
import AuthUtil from '@/sheep/api/member/auth';

// 1. 表单响应式数据归类
const formData = reactive({
  phone: '',
  code: '',
  password: '',
  confirmPassword: '',
});

// 2. 交互与状态控制
const isSending = ref(false);
const isSubmitting = ref(false);
const countdown = ref(0);
const isModify = ref(false);
let timer = null;

// 计算属性:验证码按钮禁用状态
const isCodeBtnDisabled = computed(() => countdown.value > 0 || isSending.value);

// 3. 正则表达式配置
const REGEX = {
  PHONE: /^1[3-9]\d{9}$/,
  STRONG_PWD: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&._#^&+=*!~-])[A-Za-z\d@$!%*?&._#^&+=*!~-]{8,16}$/,
};

/** Toast 提示封装 */
const showToast = (title, icon = 'none') => {
  uni.showToast({ title, icon });
};

/** 清除倒计时定时器 */
const clearCountdownTimer = () => {
  if (timer) {
    clearInterval(timer);
    timer = null;
  }
};

/** 开启倒计时 */
const startCountdown = (seconds = 60) => {
  clearCountdownTimer();
  countdown.value = seconds;
  timer = setInterval(() => {
    countdown.value--;
    if (countdown.value <= 0) {
      clearCountdownTimer();
    }
  }, 1000);
};

// 4. 事件处理:发送短信验证码
const handleSendCode = async () => {
  if (isCodeBtnDisabled.value) return;

  if (!formData.phone) {
    return showToast('请输入手机号码');
  }
  if (!REGEX.PHONE.test(formData.phone)) {
    return showToast('请输入正确的11位手机号码');
  }

  isSending.value = true;
  try {
    const res = await AuthUtil.sendSmsCode({ phone: formData.phone });
    if (res && (res.code === 0 || res.data === true)) {
      showToast('验证码已发送', 'success');
      startCountdown(60);
    } else {
      showToast(res?.msg || '获取验证码失败');
    }
  } catch (err) {
    console.error('发送验证码失败异常:', err);
    showToast('网络开小差了,请稍后再试');
  } finally {
    isSending.value = false;
  }
};

// 5. 事件处理:提交修改密码
const handleSubmit = async () => {
  if (isSubmitting.value) return;

  // 表单完整性校验
  if (!formData.phone || !REGEX.PHONE.test(formData.phone)) {
    return showToast('请输入正确的11位手机号码');
  }
  if (!formData.code || formData.code.length < 4) {
    return showToast('请输入正确的短信验证码');
  }
  if (!formData.password) {
    return showToast('请输入您的新密码');
  }
  // 如需强密码校验,取消下一行注释:
  // if (!REGEX.STRONG_PWD.test(formData.password)) {
  //   return showToast('密码须为8~16位并包含大小写字母、数字及特殊字符');
  // }
  if (formData.password !== formData.confirmPassword) {
    return showToast('两次输入的密码不一致');
  }

  isSubmitting.value = true;
  uni.showLoading({ title: '正在修改密码...', mask: true });

  try {
    const res = await AuthUtil.setPassword({
      phone: formData.phone,
      code: formData.code,
      password: formData.password,
    });

    if (res && res.code === 0) {
      showToast('密码修改成功', 'success');
      setTimeout(() => {
        uni.redirectTo({
          url: '/pages7/pages/index/login',
        });
      }, 1500);
    } else {
      showToast(res?.msg || '修改失败,请重试');
    }
  } catch (err) {
    console.error('重置密码运行异常:', err);
    showToast('网络繁忙,请稍后再试');
  } finally {
    isSubmitting.value = false;
    uni.hideLoading();
  }
};

// 6. 生命周期管理
onLoad((options) => {
  isModify.value = options?.isModify === 'true';
  const pageTitle = isModify.value ? '修改密码' : '找回密码';
  uni.setNavigationBarTitle({ title: pageTitle });
});

// 清理定时器,防止泄漏
onHide(clearCountdownTimer);
onUnload(clearCountdownTimer);
onBeforeUnmount(clearCountdownTimer);
</script>

<style lang="scss" scoped>
.login-wrapper {
  height: 100vh;
  // #ifdef H5
  height: calc(100vh - 44px);
  // #endif
  background-color: #ffffff;
  padding: 40rpx 50rpx;
  box-sizing: border-box;

  .form-container {
    margin-top: 20rpx;

    .form-item-wrapper {
      margin-bottom: 36rpx;

      .form-label {
        font-size: 26rpx;
        color: #0a1931;
        font-weight: bold;
        display: block;
        margin-bottom: 16rpx;
      }

      .input-hint {
        display: block;
        font-size: 24rpx;
        color: #9097a3;
        line-height: 1.5;
        margin-top: 12rpx;
        padding: 0 10rpx;
      }

      .input-box {
        height: 104rpx;
        background-color: #f4f6fa;
        border: 2rpx solid #e1e6eb;
        border-radius: 24rpx;
        display: flex;
        align-items: center;
        padding: 0 20rpx;
        box-sizing: border-box;

        .custom-u-input {
          width: 100%;
          height: 100%;
          font-size: 28rpx;
        }

        .input-icon {
          margin-right: 12rpx;
        }
      }

      /* 双列布局:验证码 */
      .code-row {
        display: flex;
        align-items: center;

        .flex-1 {
          flex: 1;
        }

        .code-btn {
          width: 220rpx;
          height: 104rpx;
          border-radius: 24rpx;
          background-color: #f9fdf2;
          border: 2rpx solid #d0ee9c;
          display: flex;
          justify-content: center;
          align-items: center;
          margin-left: 20rpx;
          font-size: 26rpx;
          color: #8fc31f;
          font-weight: bold;
          padding: 0;
          line-height: normal;
          transition: all 0.2s ease;

          &::after {
            border: none;
          }

          &:active {
            opacity: 0.8;
          }

          &.disabled {
            color: #b0b8c4;
            background-color: #f4f6fa;
            border-color: #e1e6eb;
            pointer-events: none;
          }
        }
      }
    }
  }

  .submit-btn {
    height: 104rpx;
    border-radius: 24rpx;
    background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
    box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
    display: flex;
    justify-content: center;
    align-items: center;
    color: #ffffff;
    font-size: 32rpx;
    font-weight: bold;
    letter-spacing: 2rpx;
    margin-top: 80rpx;
    transition: all 0.2s ease;

    &:active {
      transform: scale(0.98);
      opacity: 0.9;
    }

    &.disabled-btn {
      opacity: 0.75;
      pointer-events: none;
    }
  }
}
</style>