dramaling-app/apps/web/src/views/auth/ForgotPasswordView.vue

327 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="forgot-password-view">
<BaseCard class="forgot-password-card">
<template #header>
<div class="back-button">
<QBtn
flat
round
icon="arrow_back"
size="sm"
@click="router.back()"
aria-label="返回上一頁"
/>
</div>
<h2 class="forgot-password-title">忘記密碼</h2>
<p class="forgot-password-subtitle">
輸入你的電子郵件地址我們將發送重設密碼的連結給你
</p>
</template>
<div v-if="!emailSent" class="forgot-password-form">
<form @submit.prevent="handleSendResetEmail">
<BaseInput
v-model="email"
type="email"
label="電子郵件"
placeholder="請輸入你的電子郵件地址"
prefix-icon="email"
:error="emailError"
:disabled="isLoading"
required
/>
<BaseButton
type="submit"
variant="primary"
size="lg"
block
:loading="isLoading"
:disabled="!canSubmit"
>
發送重設連結
</BaseButton>
</form>
</div>
<div v-else class="success-message">
<div class="success-icon">
<QIcon name="mark_email_read" />
</div>
<h3>郵件已發送</h3>
<p>
我們已將重設密碼的連結發送到 <strong>{{ email }}</strong>
</p>
<p class="instruction">
請檢查你的收件匣(也可能在垃圾郵件資料夾中),點擊連結來重設密碼。
</p>
<div class="resend-section">
<p class="resend-text">沒有收到郵件?</p>
<BaseButton
variant="outline"
size="md"
:disabled="resendCooldown > 0 || isLoading"
@click="handleResendEmail"
>
<span v-if="resendCooldown > 0">
重新發送 ({{ resendCooldown }}s)
</span>
<span v-else>重新發送</span>
</BaseButton>
</div>
</div>
<template #footer>
<div class="login-prompt">
想起密碼了?
<router-link to="/auth/login" class="login-link">
返回登入
</router-link>
</div>
</template>
</BaseCard>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useUIStore } from '@/stores/ui'
import BaseCard from '@/components/base/BaseCard.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import { isValidEmail } from '@/utils'
const router = useRouter()
const authStore = useAuthStore()
const uiStore = useUIStore()
// 狀態
const email = ref('')
const emailError = ref('')
const isLoading = ref(false)
const emailSent = ref(false)
const resendCooldown = ref(0)
// 計算屬性
const canSubmit = computed(() => {
return email.value && isValidEmail(email.value) && !isLoading.value
})
// 方法
const validateEmail = () => {
emailError.value = ''
if (!email.value) {
emailError.value = '請輸入電子郵件地址'
return false
}
if (!isValidEmail(email.value)) {
emailError.value = '請輸入有效的電子郵件地址'
return false
}
return true
}
const handleSendResetEmail = async () => {
if (!validateEmail()) return
isLoading.value = true
try {
const result = await authStore.forgotPassword(email.value)
if (result.success) {
emailSent.value = true
uiStore.showSuccessToast('郵件已發送', '請檢查你的收件匣')
startResendCooldown()
} else {
uiStore.showErrorToast('發送失敗', result.error)
if (result.error?.includes('email')) {
emailError.value = result.error
}
}
} catch (error) {
uiStore.showErrorToast('發送失敗', '發生未知錯誤,請稍後再試')
} finally {
isLoading.value = false
}
}
const handleResendEmail = async () => {
if (resendCooldown.value > 0) return
isLoading.value = true
try {
const result = await authStore.forgotPassword(email.value)
if (result.success) {
uiStore.showSuccessToast('郵件已重新發送', '請檢查你的收件匣')
startResendCooldown()
} else {
uiStore.showErrorToast('重新發送失敗', result.error)
}
} catch (error) {
uiStore.showErrorToast('重新發送失敗', '發生未知錯誤,請稍後再試')
} finally {
isLoading.value = false
}
}
const startResendCooldown = () => {
resendCooldown.value = 60 // 60秒冷卻時間
const countdown = setInterval(() => {
resendCooldown.value -= 1
if (resendCooldown.value <= 0) {
clearInterval(countdown)
}
}, 1000)
}
</script>
<style lang="scss" scoped>
.forgot-password-view {
width: 100%;
max-width: 400px;
margin: 0 auto;
}
.forgot-password-card {
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba($divider, 0.2);
backdrop-filter: blur(10px);
position: relative;
}
.back-button {
position: absolute;
top: -$space-2;
left: -$space-2;
.q-btn {
color: $text-secondary;
&:hover {
color: $primary-teal;
background: rgba($primary-teal, 0.1);
}
}
}
.forgot-password-title {
font-size: $text-2xl;
font-weight: 700;
color: $text-primary;
margin: 0 0 $space-3 0;
text-align: center;
}
.forgot-password-subtitle {
font-size: $text-sm;
color: $text-secondary;
margin: 0;
text-align: center;
line-height: 1.5;
}
.forgot-password-form {
display: flex;
flex-direction: column;
gap: $space-6;
}
.success-message {
text-align: center;
.success-icon {
margin-bottom: $space-4;
.q-icon {
font-size: 64px;
color: $success-green;
}
}
h3 {
font-size: $text-xl;
font-weight: 700;
color: $text-primary;
margin: 0 0 $space-4 0;
}
p {
font-size: $text-base;
color: $text-secondary;
margin: 0 0 $space-3 0;
line-height: 1.5;
strong {
color: $text-primary;
word-break: break-word;
}
}
.instruction {
font-size: $text-sm;
color: $text-tertiary;
padding: $space-4;
background: rgba($primary-teal, 0.05);
border-radius: $radius-md;
border-left: 4px solid $primary-teal;
}
.resend-section {
margin-top: $space-6;
padding-top: $space-4;
border-top: 1px solid rgba($divider, 0.3);
.resend-text {
font-size: $text-sm;
margin-bottom: $space-3;
}
}
}
.login-prompt {
text-align: center;
font-size: $text-sm;
color: $text-secondary;
.login-link {
color: $primary-teal;
text-decoration: none;
font-weight: 600;
margin-left: $space-1;
transition: color 0.3s ease;
&:hover {
color: $primary-teal-light;
text-decoration: underline;
}
}
}
// 響應式設計
@include respond-to(xs) {
.forgot-password-view {
padding: $space-4;
}
.back-button {
top: $space-2;
left: $space-2;
}
.forgot-password-title {
margin-top: $space-8;
}
}
</style>