|
|
@@ -0,0 +1,286 @@
|
|
|
+<template>
|
|
|
+ <div class="forgot-password-page">
|
|
|
+ <!-- 顶部导航 -->
|
|
|
+ <van-nav-bar
|
|
|
+ :title="$t('auth.resetPassword')"
|
|
|
+ left-arrow
|
|
|
+ @click-left="goBack"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 表单 -->
|
|
|
+ <div class="form-section">
|
|
|
+ <div class="form-content">
|
|
|
+ <div class="input-group">
|
|
|
+ <van-field
|
|
|
+ v-model="form.phone"
|
|
|
+ type="tel"
|
|
|
+ :placeholder="$t('auth.pleaseInputPhone')"
|
|
|
+ clearable
|
|
|
+ >
|
|
|
+ <template #left-icon>
|
|
|
+ <div class="area-code" @click="showAreaCode = true">
|
|
|
+ +{{ areaCode }}
|
|
|
+ <van-icon name="arrow-down" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </van-field>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="input-group">
|
|
|
+ <van-field
|
|
|
+ v-model="form.code"
|
|
|
+ type="number"
|
|
|
+ :placeholder="$t('auth.pleaseInputVerifyCode')"
|
|
|
+ maxlength="6"
|
|
|
+ >
|
|
|
+ <template #button>
|
|
|
+ <van-button
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ :disabled="countdown > 0"
|
|
|
+ @click="sendCode"
|
|
|
+ >
|
|
|
+ {{ countdown > 0 ? `${countdown}s` : $t('auth.getCode') }}
|
|
|
+ </van-button>
|
|
|
+ </template>
|
|
|
+ </van-field>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="input-group">
|
|
|
+ <van-field
|
|
|
+ v-model="form.newPassword"
|
|
|
+ :type="showPassword ? 'text' : 'password'"
|
|
|
+ :placeholder="$t('auth.newPassword')"
|
|
|
+ >
|
|
|
+ <template #right-icon>
|
|
|
+ <van-icon
|
|
|
+ :name="showPassword ? 'eye-o' : 'closed-eye'"
|
|
|
+ @click="showPassword = !showPassword"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </van-field>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="input-group">
|
|
|
+ <van-field
|
|
|
+ v-model="form.confirmPassword"
|
|
|
+ :type="showConfirmPassword ? 'text' : 'password'"
|
|
|
+ :placeholder="$t('auth.confirmPassword')"
|
|
|
+ >
|
|
|
+ <template #right-icon>
|
|
|
+ <van-icon
|
|
|
+ :name="showConfirmPassword ? 'eye-o' : 'closed-eye'"
|
|
|
+ @click="showConfirmPassword = !showConfirmPassword"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </van-field>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <van-button
|
|
|
+ class="submit-btn"
|
|
|
+ type="primary"
|
|
|
+ block
|
|
|
+ :loading="loading"
|
|
|
+ @click="handleSubmit"
|
|
|
+ >
|
|
|
+ {{ $t('auth.resetPassword') }}
|
|
|
+ </van-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 区号选择弹窗 -->
|
|
|
+ <van-popup v-model:show="showAreaCode" position="bottom" round>
|
|
|
+ <van-picker
|
|
|
+ :columns="areaCodes"
|
|
|
+ @confirm="onAreaCodeConfirm"
|
|
|
+ @cancel="showAreaCode = false"
|
|
|
+ show-toolbar
|
|
|
+ />
|
|
|
+ </van-popup>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive } from 'vue';
|
|
|
+import { useRouter } from 'vue-router';
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
+import { toast } from 'vue-sonner';
|
|
|
+import { requestSendCode, requestResetPassword } from '@/api/auth';
|
|
|
+
|
|
|
+const { t } = useI18n();
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const showPassword = ref(false);
|
|
|
+const showConfirmPassword = ref(false);
|
|
|
+const showAreaCode = ref(false);
|
|
|
+const areaCode = ref('84');
|
|
|
+const countdown = ref(0);
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ phone: '',
|
|
|
+ code: '',
|
|
|
+ newPassword: '',
|
|
|
+ confirmPassword: ''
|
|
|
+});
|
|
|
+
|
|
|
+const areaCodes = [
|
|
|
+ { text: '+84 Vietnam', value: '84' },
|
|
|
+ { text: '+62 Indonesia', value: '62' },
|
|
|
+ { text: '+86 China', value: '86' },
|
|
|
+ { text: '+1 USA', value: '1' }
|
|
|
+];
|
|
|
+
|
|
|
+const onAreaCodeConfirm = ({ selectedValues }) => {
|
|
|
+ areaCode.value = selectedValues[0];
|
|
|
+ showAreaCode.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+const goBack = () => {
|
|
|
+ router.back();
|
|
|
+};
|
|
|
+
|
|
|
+// 发送验证码
|
|
|
+const sendCode = async () => {
|
|
|
+ if (!form.phone) {
|
|
|
+ toast.error(t('auth.pleaseInputPhone'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await requestSendCode({
|
|
|
+ type: 'phone',
|
|
|
+ account: form.phone,
|
|
|
+ areaCode: areaCode.value,
|
|
|
+ scene: 'reset'
|
|
|
+ });
|
|
|
+ if (res.code === 200) {
|
|
|
+ toast.success(t('common.success') || '验证码已发送');
|
|
|
+ countdown.value = 60;
|
|
|
+ const timer = setInterval(() => {
|
|
|
+ countdown.value--;
|
|
|
+ if (countdown.value <= 0) {
|
|
|
+ clearInterval(timer);
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Send code error:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 提交重置密码
|
|
|
+const handleSubmit = async () => {
|
|
|
+ if (!form.phone) {
|
|
|
+ toast.error(t('auth.pleaseInputPhone'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!form.code) {
|
|
|
+ toast.error(t('auth.pleaseInputVerifyCode'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!form.newPassword) {
|
|
|
+ toast.error(t('auth.pleaseInputPassword'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (form.newPassword.length < 6) {
|
|
|
+ toast.error(t('auth.passwordTooShort') || '密码至少6位');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (form.newPassword !== form.confirmPassword) {
|
|
|
+ toast.error(t('auth.passwordNotMatch'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await requestResetPassword({
|
|
|
+ type: 'phone',
|
|
|
+ account: form.phone,
|
|
|
+ code: form.code,
|
|
|
+ newPassword: form.newPassword,
|
|
|
+ areaCode: areaCode.value
|
|
|
+ });
|
|
|
+ if (res.code === 200) {
|
|
|
+ toast.success(t('auth.resetSuccess') || '密码重置成功');
|
|
|
+ router.replace('/login');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.forgot-password-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
|
|
+
|
|
|
+ :deep(.van-nav-bar) {
|
|
|
+ background: transparent;
|
|
|
+
|
|
|
+ .van-nav-bar__title {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .van-nav-bar__arrow {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.form-section {
|
|
|
+ padding: 40px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-content {
|
|
|
+ .input-group {
|
|
|
+ margin-bottom: 16px;
|
|
|
+
|
|
|
+ :deep(.van-field) {
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px 16px;
|
|
|
+
|
|
|
+ .van-field__control {
|
|
|
+ color: #fff;
|
|
|
+
|
|
|
+ &::placeholder {
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .van-icon {
|
|
|
+ color: rgba(255, 255, 255, 0.6);
|
|
|
+ }
|
|
|
+
|
|
|
+ .van-button--primary {
|
|
|
+ background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
|
|
|
+ border: none;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .area-code {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ color: #fff;
|
|
|
+ padding-right: 12px;
|
|
|
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
+ margin-right: 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.submit-btn {
|
|
|
+ margin-top: 32px;
|
|
|
+ height: 48px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+</style>
|