361 lines
7.3 KiB
Plaintext
361 lines
7.3 KiB
Plaintext
<template>
|
|
<view class="page-container">
|
|
<scroll-view class="content-scroll" direction="vertical">
|
|
<!-- 步骤指示器 -->
|
|
<view class="steps-bar">
|
|
<view class="step-item">
|
|
<view :class="step >= 1 ? 'step-dot-active' : 'step-dot'"></view>
|
|
<text :class="step >= 1 ? 'step-label-active' : 'step-label'">验证手机</text>
|
|
</view>
|
|
<view class="step-line"></view>
|
|
<view class="step-item">
|
|
<view :class="step >= 2 ? 'step-dot-active' : 'step-dot'"></view>
|
|
<text :class="step >= 2 ? 'step-label-active' : 'step-label'">设置密码</text>
|
|
</view>
|
|
<view class="step-line"></view>
|
|
<view class="step-item">
|
|
<view :class="step >= 3 ? 'step-dot-active' : 'step-dot'"></view>
|
|
<text :class="step >= 3 ? 'step-label-active' : 'step-label'">完成</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 步骤一: 验证手机号 -->
|
|
<view v-if="step === 1" class="form-section">
|
|
<text class="section-title">验证手机号</text>
|
|
<text class="section-desc">请输入注册时使用的手机号</text>
|
|
|
|
<view class="input-wrapper">
|
|
<input
|
|
class="input-field"
|
|
type="number"
|
|
v-model="phone"
|
|
placeholder="请输入手机号"
|
|
maxlength="11"
|
|
/>
|
|
</view>
|
|
|
|
<view class="input-wrapper">
|
|
<input
|
|
class="input-field"
|
|
type="number"
|
|
v-model="code"
|
|
placeholder="请输入验证码"
|
|
maxlength="6"
|
|
/>
|
|
<countdown-btn
|
|
ref="countdownBtnRef"
|
|
:phone="phone"
|
|
codeType="forgot-password"
|
|
@send="handleSendCode"
|
|
/>
|
|
</view>
|
|
|
|
<view
|
|
class="btn-main"
|
|
:class="canStep1 ? 'btn-active' : 'btn-disabled'"
|
|
@tap="handleStep1"
|
|
>
|
|
<text class="btn-text">下一步</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 步骤二: 设置新密码 -->
|
|
<view v-if="step === 2" class="form-section">
|
|
<text class="section-title">设置新密码</text>
|
|
<text class="section-desc">请设置6-20位新密码</text>
|
|
|
|
<view class="input-wrapper">
|
|
<input
|
|
class="input-field"
|
|
type="password"
|
|
v-model="newPassword"
|
|
placeholder="请输入新密码(6-20位)"
|
|
maxlength="20"
|
|
/>
|
|
</view>
|
|
|
|
<view class="input-wrapper">
|
|
<input
|
|
class="input-field"
|
|
type="password"
|
|
v-model="confirmPassword"
|
|
placeholder="请再次输入新密码"
|
|
maxlength="20"
|
|
/>
|
|
</view>
|
|
|
|
<view
|
|
class="btn-main"
|
|
:class="canStep2 ? 'btn-active' : 'btn-disabled'"
|
|
@tap="handleStep2"
|
|
>
|
|
<text class="btn-text">完成</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 步骤三: 完成 -->
|
|
<view v-if="step === 3" class="success-section">
|
|
<view class="success-icon-wrap">
|
|
<view class="success-mark"></view>
|
|
</view>
|
|
<text class="success-title">密码重置成功</text>
|
|
<text class="success-desc">请使用新密码登录</text>
|
|
|
|
<view class="btn-main btn-active" @tap="handleGoLogin">
|
|
<text class="btn-text">去登录</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 返回登录 -->
|
|
<text class="back-text" @tap="handleBack">返回登录</text>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { apiSendCode, apiVerifyCode, apiResetPassword } from '../../api/auth'
|
|
import CountdownBtn from '../../components/countdown-btn.uvue'
|
|
|
|
const step = ref(1)
|
|
const phone = ref('')
|
|
const code = ref('')
|
|
const resetToken = ref('')
|
|
const newPassword = ref('')
|
|
const confirmPassword = ref('')
|
|
const countdownBtnRef = ref<any>(null)
|
|
|
|
const canStep1 = computed((): boolean => {
|
|
return /^1[3-9]\d{9}$/.test(phone.value) && code.value.length === 6
|
|
})
|
|
|
|
const canStep2 = computed((): boolean => {
|
|
return (
|
|
newPassword.value.length >= 6 &&
|
|
newPassword.value.length <= 20 &&
|
|
newPassword.value === confirmPassword.value
|
|
)
|
|
})
|
|
|
|
async function handleSendCode(p: string) {
|
|
if (!/^1[3-9]\d{9}$/.test(p)) {
|
|
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
|
return
|
|
}
|
|
const res = await apiSendCode({ phone: p, type: 'forgot-password' })
|
|
if (res && res.code === 200) {
|
|
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
|
countdownBtnRef.value?.startCountdown()
|
|
}
|
|
}
|
|
|
|
async function handleStep1() {
|
|
if (!canStep1.value) return
|
|
const res = await apiVerifyCode({ phone: phone.value, code: code.value })
|
|
if (res && res.data) {
|
|
resetToken.value = res.data.token
|
|
step.value = 2
|
|
}
|
|
}
|
|
|
|
async function handleStep2() {
|
|
if (!canStep2.value) {
|
|
if (newPassword.value !== confirmPassword.value) {
|
|
uni.showToast({ title: '两次密码输入不一致', icon: 'none' })
|
|
}
|
|
return
|
|
}
|
|
|
|
const res = await apiResetPassword({
|
|
phone: phone.value,
|
|
newPassword: newPassword.value,
|
|
resetToken: resetToken.value
|
|
})
|
|
|
|
if (res && res.code === 200) {
|
|
step.value = 3
|
|
}
|
|
}
|
|
|
|
function handleGoLogin() {
|
|
uni.navigateTo({ url: '/pages/login/index' })
|
|
}
|
|
|
|
function handleBack() {
|
|
uni.navigateBack()
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.page-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
background-color: #FFFFFF;
|
|
}
|
|
|
|
.content-scroll {
|
|
flex: 1;
|
|
padding: 24px 24px calc(32px + var(--uni-safe-area-inset-bottom));
|
|
}
|
|
|
|
/* 步骤条 */
|
|
.steps-bar {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.step-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.step-dot {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 12px;
|
|
background-color: #E5E7EB;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.step-dot-active {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 12px;
|
|
background-color: #6C63FF;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.step-label {
|
|
font-size: 12px;
|
|
color: #9CA3AF;
|
|
}
|
|
|
|
.step-label-active {
|
|
font-size: 12px;
|
|
color: #6C63FF;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.step-line {
|
|
flex: 1;
|
|
height: 2px;
|
|
background-color: #E5E7EB;
|
|
margin: 0 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* 表单 */
|
|
.form-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #1F2937;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.section-desc {
|
|
font-size: 14px;
|
|
color: #6B7280;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
.input-wrapper {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
height: 52px;
|
|
background-color: #F9FAFB;
|
|
border-radius: 12px;
|
|
padding: 0 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.input-field {
|
|
flex: 1;
|
|
font-size: 16px;
|
|
color: #1F2937;
|
|
}
|
|
|
|
/* 按钮 */
|
|
.btn-main {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 52px;
|
|
border-radius: 26px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.btn-active {
|
|
background-color: #6C63FF;
|
|
}
|
|
|
|
.btn-disabled {
|
|
background-color: #E5E7EB;
|
|
}
|
|
|
|
.btn-text {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
/* 成功页 */
|
|
.success-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding-top: 40px;
|
|
}
|
|
|
|
.success-icon-wrap {
|
|
width: 80px;
|
|
height: 80px;
|
|
background-color: #ECFDF5;
|
|
border-radius: 40px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.success-mark {
|
|
width: 28px;
|
|
height: 14px;
|
|
border-left: 4px solid #10B981;
|
|
border-bottom: 4px solid #10B981;
|
|
transform: rotate(-45deg);
|
|
}
|
|
|
|
.success-title {
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
color: #1F2937;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.success-desc {
|
|
font-size: 14px;
|
|
color: #6B7280;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
/* 返回 */
|
|
.back-text {
|
|
font-size: 14px;
|
|
color: #6C63FF;
|
|
text-align: center;
|
|
margin-top: 20px;
|
|
}
|
|
</style>
|