382 lines
7.4 KiB
Vue
382 lines
7.4 KiB
Vue
<template>
|
||
<view class="forgot-password-container">
|
||
<view class="forgot-password-content">
|
||
<!-- 头部 -->
|
||
<view class="header">
|
||
|
||
|
||
</view>
|
||
|
||
<!-- 表单区域 -->
|
||
<view class="form-section">
|
||
<view class="input-group">
|
||
<view class="input-icon">📱</view>
|
||
<input
|
||
class="input-field"
|
||
v-model="phone"
|
||
type="number"
|
||
maxlength="11"
|
||
placeholder="请输入手机号"
|
||
/>
|
||
</view>
|
||
|
||
<view class="input-group">
|
||
<view class="input-icon">✉️</view>
|
||
<input
|
||
class="input-field"
|
||
v-model="code"
|
||
type="number"
|
||
maxlength="6"
|
||
placeholder="请输入验证码"
|
||
/>
|
||
<button class="code-btn" @click="sendCode" :disabled="counting">
|
||
{{ codeText }}
|
||
</button>
|
||
</view>
|
||
|
||
<view class="input-group">
|
||
<view class="input-icon">🔐</view>
|
||
<input
|
||
class="input-field"
|
||
v-model="newPassword"
|
||
:password="!showPassword"
|
||
placeholder="请输入新密码(6-20位)"
|
||
/>
|
||
<view class="eye-icon" @click="togglePassword">
|
||
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="input-group">
|
||
<view class="input-icon">🔐</view>
|
||
<input
|
||
class="input-field"
|
||
v-model="confirmPassword"
|
||
:password="!showConfirmPassword"
|
||
placeholder="请确认新密码"
|
||
/>
|
||
<view class="eye-icon" @click="toggleConfirmPassword">
|
||
{{ showConfirmPassword ? '👁️' : '👁️🗨️' }}
|
||
</view>
|
||
</view>
|
||
|
||
<button class="reset-btn" @click="handleReset" :disabled="loading">
|
||
<text v-if="loading">重置中...</text>
|
||
<text v-else>重置密码</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { API_BASE } from '@/config/api.js';
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
phone: '',
|
||
code: '',
|
||
newPassword: '',
|
||
confirmPassword: '',
|
||
showPassword: false,
|
||
showConfirmPassword: false,
|
||
loading: false,
|
||
counting: false,
|
||
countdown: 60,
|
||
codeText: '获取验证码'
|
||
};
|
||
},
|
||
methods: {
|
||
// 返回
|
||
goBack() {
|
||
uni.navigateBack();
|
||
},
|
||
|
||
// 切换密码显示
|
||
togglePassword() {
|
||
this.showPassword = !this.showPassword;
|
||
},
|
||
|
||
toggleConfirmPassword() {
|
||
this.showConfirmPassword = !this.showConfirmPassword;
|
||
},
|
||
|
||
// 发送验证码
|
||
async sendCode() {
|
||
if (!this.phone) {
|
||
uni.showToast({
|
||
title: '请输入手机号',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!/^1[3-9]\d{9}$/.test(this.phone)) {
|
||
uni.showToast({
|
||
title: '手机号格式不正确',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await uni.request({
|
||
url: `${API_BASE}/api/sms/send`,
|
||
method: 'POST',
|
||
header: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
data: {
|
||
phone: this.phone,
|
||
purpose: 'RESET_PASSWORD'
|
||
}
|
||
});
|
||
|
||
const { statusCode, data } = res[1] || res;
|
||
|
||
if (statusCode === 200) {
|
||
// 开始倒计时
|
||
this.counting = true;
|
||
this.countdown = 60;
|
||
|
||
const timer = setInterval(() => {
|
||
this.countdown--;
|
||
this.codeText = `${this.countdown}秒后重试`;
|
||
|
||
if (this.countdown <= 0) {
|
||
clearInterval(timer);
|
||
this.counting = false;
|
||
this.codeText = '获取验证码';
|
||
}
|
||
}, 1000);
|
||
|
||
uni.showToast({
|
||
title: '验证码已发送',
|
||
icon: 'success'
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: data?.message || '发送失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('[ForgotPassword] 发送验证码失败:', err);
|
||
uni.showToast({
|
||
title: '网络错误,请重试',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 重置密码
|
||
async handleReset() {
|
||
if (!this.phone) {
|
||
uni.showToast({
|
||
title: '请输入手机号',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!/^1[3-9]\d{9}$/.test(this.phone)) {
|
||
uni.showToast({
|
||
title: '手机号格式不正确',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!this.code) {
|
||
uni.showToast({
|
||
title: '请输入验证码',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!this.newPassword) {
|
||
uni.showToast({
|
||
title: '请输入新密码',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (this.newPassword.length < 6 || this.newPassword.length > 20) {
|
||
uni.showToast({
|
||
title: '密码长度为6-20位',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (this.newPassword !== this.confirmPassword) {
|
||
uni.showToast({
|
||
title: '两次密码不一致',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
this.loading = true;
|
||
|
||
try {
|
||
const res = await uni.request({
|
||
url: `${API_BASE}/api/users/forgot-password`,
|
||
method: 'POST',
|
||
header: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
data: {
|
||
phone: this.phone,
|
||
code: this.code,
|
||
newPassword: this.newPassword
|
||
}
|
||
});
|
||
|
||
const { statusCode, data } = res[1] || res;
|
||
|
||
if (statusCode === 204 || statusCode === 200) {
|
||
uni.showToast({
|
||
title: '密码重置成功',
|
||
icon: 'success',
|
||
duration: 1500
|
||
});
|
||
|
||
setTimeout(() => {
|
||
uni.navigateBack();
|
||
}, 1500);
|
||
} else {
|
||
uni.showToast({
|
||
title: data?.message || '重置失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('[ForgotPassword] 重置密码失败:', err);
|
||
uni.showToast({
|
||
title: '网络错误,请重试',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.forgot-password-container {
|
||
min-height: 100vh;
|
||
background: #FDF8F2;
|
||
background-image:
|
||
radial-gradient(circle at 10% 20%, rgba(212, 185, 150, 0.1) 0%, transparent 20%),
|
||
radial-gradient(circle at 90% 80%, rgba(109, 139, 139, 0.1) 0%, transparent 20%);
|
||
padding-top: env(safe-area-inset-top);
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.forgot-password-content {
|
||
padding: 40upx;
|
||
}
|
||
|
||
/* 头部 */
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 60upx;
|
||
|
||
.back-btn {
|
||
font-size: 28upx;
|
||
color: #8B7355;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 36upx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-right: 80upx;
|
||
}
|
||
}
|
||
|
||
/* 表单区域 */
|
||
.form-section {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 40upx;
|
||
padding: 60upx 40upx;
|
||
box-shadow: 0 16upx 60upx rgba(0, 0, 0, 0.08);
|
||
backdrop-filter: blur(20upx);
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
align-items: center;
|
||
background: #f5f5f5;
|
||
border-radius: 30upx;
|
||
padding: 0 30upx;
|
||
margin-bottom: 30upx;
|
||
height: 100upx;
|
||
|
||
.input-icon {
|
||
font-size: 40upx;
|
||
margin-right: 20upx;
|
||
}
|
||
|
||
.input-field {
|
||
flex: 1;
|
||
font-size: 30upx;
|
||
color: #333;
|
||
}
|
||
|
||
.eye-icon {
|
||
font-size: 40upx;
|
||
padding: 10upx;
|
||
}
|
||
|
||
.code-btn {
|
||
padding: 12upx 24upx;
|
||
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
|
||
color: #ffffff;
|
||
border: none;
|
||
border-radius: 20upx;
|
||
font-size: 24upx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.code-btn[disabled] {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
.reset-btn {
|
||
width: 100%;
|
||
height: 100upx;
|
||
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
|
||
color: #ffffff;
|
||
border: none;
|
||
border-radius: 50upx;
|
||
font-size: 32upx;
|
||
font-weight: bold;
|
||
box-shadow: 0 16upx 40upx rgba(139, 115, 85, 0.3);
|
||
margin-top: 20upx;
|
||
|
||
&:active {
|
||
transform: translateY(-2upx);
|
||
box-shadow: 0 20upx 50upx rgba(139, 115, 85, 0.4);
|
||
}
|
||
}
|
||
|
||
.reset-btn[disabled] {
|
||
opacity: 0.6;
|
||
transform: none !important;
|
||
}
|
||
</style>
|