111 lines
3.6 KiB
Vue
111 lines
3.6 KiB
Vue
<template>
|
||
<div class="forgot-page">
|
||
<div class="forgot-card">
|
||
<div class="logo">🌿</div>
|
||
<h2>找回密码</h2>
|
||
<p class="subtitle">输入注册邮箱,验证码将发送(开发期打印在服务器日志)</p>
|
||
|
||
<n-form>
|
||
<n-form-item label="注册邮箱">
|
||
<n-input v-model:value="email" placeholder="your@email.com" size="large" />
|
||
</n-form-item>
|
||
|
||
<template v-if="step === 2">
|
||
<n-form-item label="验证码(见后端日志)">
|
||
<n-input v-model:value="code" placeholder="6 位验证码" size="large" maxlength="6" />
|
||
</n-form-item>
|
||
<n-form-item label="新密码">
|
||
<n-input v-model:value="newPassword" type="password" show-password-on="click" placeholder="新密码" size="large" />
|
||
</n-form-item>
|
||
</template>
|
||
|
||
<n-button v-if="step === 1" type="primary" block size="large" :loading="sending" @click="sendCode">
|
||
发送验证码
|
||
</n-button>
|
||
<n-button v-else type="primary" block size="large" :loading="resetting" @click="reset">
|
||
重置密码
|
||
</n-button>
|
||
</n-form>
|
||
|
||
<div class="footer">
|
||
<router-link to="/login">← 返回登录</router-link>
|
||
</div>
|
||
|
||
<div v-if="devHint" class="dev-hint">
|
||
💡 开发模式:验证码已打印到后端控制台(docker compose logs backend)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useMessage } from 'naive-ui'
|
||
import api from '@/api/client'
|
||
|
||
const router = useRouter()
|
||
const message = useMessage()
|
||
const email = ref('')
|
||
const code = ref('')
|
||
const newPassword = ref('')
|
||
const step = ref(1)
|
||
const sending = ref(false)
|
||
const resetting = ref(false)
|
||
const devHint = ref(false)
|
||
|
||
async function sendCode() {
|
||
if (!email.value.trim()) { message.error('请输入邮箱'); return }
|
||
sending.value = true
|
||
try {
|
||
await api.post('/auth/forgot', { email: email.value.trim() })
|
||
step.value = 2
|
||
devHint.value = true
|
||
message.success('验证码已发送(开发期见后端日志)')
|
||
} catch (e: any) {
|
||
message.error(e.response?.data?.detail || '发送失败')
|
||
} finally {
|
||
sending.value = false
|
||
}
|
||
}
|
||
|
||
async function reset() {
|
||
if (!code.value || !newPassword.value) { message.error('请填写完整'); return }
|
||
resetting.value = true
|
||
try {
|
||
await api.post('/auth/reset', {
|
||
email: email.value.trim(),
|
||
code: code.value.trim(),
|
||
new_password: newPassword.value,
|
||
})
|
||
message.success('密码已重置,请重新登录')
|
||
router.push('/login')
|
||
} catch (e: any) {
|
||
message.error(e.response?.data?.detail || '重置失败')
|
||
} finally {
|
||
resetting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.forgot-page {
|
||
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
||
background: linear-gradient(135deg, #009688 0%, #26A69A 100%);
|
||
padding: 20px;
|
||
}
|
||
.forgot-card {
|
||
width: 400px; max-width: 100%; background: var(--color-surface, #fff);
|
||
border-radius: 20px; padding: 36px 32px; box-shadow: 0 12px 40px rgba(0,0,0,0.15);
|
||
}
|
||
.logo { font-size: 48px; text-align: center; }
|
||
.forgot-card h2 { text-align: center; margin: 8px 0 4px; color: var(--color-primary-dark, #00796B); }
|
||
.subtitle { text-align: center; font-size: 13px; color: var(--color-text-hint, #999); margin-bottom: 24px; }
|
||
.footer { text-align: center; margin-top: 20px; }
|
||
.footer a { font-size: 13px; color: var(--color-primary, #009688); text-decoration: none; }
|
||
.dev-hint {
|
||
margin-top: 16px; padding: 10px; background: #FFF3E0; border-radius: 8px;
|
||
font-size: 12px; color: #E65100; text-align: center;
|
||
}
|
||
</style>
|