feat: 忘記密碼
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import PasswordRecoveryShell from "@/components/auth/PasswordRecoveryShell.vue";
|
||||
import ResetPasswordForm from "@/components/auth/ResetPasswordForm.vue";
|
||||
import { usePasswordReset } from "@/composables/usePasswordReset";
|
||||
const {
|
||||
hasToken,
|
||||
formVersion,
|
||||
loading,
|
||||
disabled,
|
||||
errorMessage,
|
||||
fieldErrors,
|
||||
remainingSeconds,
|
||||
submit,
|
||||
} = usePasswordReset();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PasswordRecoveryShell
|
||||
title="重設密碼"
|
||||
description="請設定新的登入密碼,完成後使用新密碼登入。"
|
||||
>
|
||||
<template v-if="!hasToken">
|
||||
<v-alert type="warning" variant="tonal" role="alert">
|
||||
密碼重設連結無效或已過期。若剛重新整理頁面,請重新開啟信件連結或重新申請密碼重設。
|
||||
</v-alert>
|
||||
<v-btn
|
||||
block
|
||||
class="mt-4"
|
||||
color="secondary"
|
||||
to="/forgot-password"
|
||||
variant="flat"
|
||||
>重新申請密碼重設</v-btn
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
class="mb-4"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
role="alert"
|
||||
>{{ errorMessage }}</v-alert
|
||||
>
|
||||
<ResetPasswordForm
|
||||
:key="formVersion"
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:errors="fieldErrors"
|
||||
:remaining-seconds="remainingSeconds"
|
||||
@submit="submit"
|
||||
/>
|
||||
</template>
|
||||
</PasswordRecoveryShell>
|
||||
</template>
|
||||
@@ -1,58 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import EmailCompletionForm from "@/components/auth/EmailCompletionForm.vue";
|
||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
||||
import { EMAIL_COMPLETION_REQUIRED } from "@/config/features";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { getApiErrorStatus } from "@/utils/api-error";
|
||||
import { shallowRef } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import EmailCompletionForm from '@/components/auth/EmailCompletionForm.vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import { EMAIL_COMPLETION_REQUIRED } from '@/config/features'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { getApiErrorStatus } from '@/utils/api-error'
|
||||
|
||||
const store = useAppStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = shallowRef(false);
|
||||
const errorMessage = shallowRef("");
|
||||
const store = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = shallowRef(false)
|
||||
const errorMessage = shallowRef('')
|
||||
|
||||
function redirectTarget() {
|
||||
const target = String(route.query.redirect ?? "");
|
||||
if (
|
||||
!target.startsWith("/") ||
|
||||
target.startsWith("//") ||
|
||||
target.startsWith("/complete-email")
|
||||
) {
|
||||
return "/";
|
||||
const target = String(route.query.redirect ?? '')
|
||||
if (!target.startsWith('/') || target.startsWith('//') || target.startsWith('/complete-email')) {
|
||||
return '/'
|
||||
}
|
||||
|
||||
return target;
|
||||
return target
|
||||
}
|
||||
|
||||
async function handleSubmit(email: string) {
|
||||
errorMessage.value = "";
|
||||
loading.value = true;
|
||||
errorMessage.value = ''
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await store.updateEmail(email);
|
||||
await router.replace(redirectTarget());
|
||||
await store.updateEmail(email)
|
||||
await router.replace(redirectTarget())
|
||||
} catch (error) {
|
||||
const status = getApiErrorStatus(error);
|
||||
const status = getApiErrorStatus(error)
|
||||
if (status === 409) {
|
||||
errorMessage.value = "使用者資料已更新,請重新整理後再試。";
|
||||
errorMessage.value = '使用者資料已更新,請重新整理後再試。'
|
||||
} else if (status === 422) {
|
||||
errorMessage.value = "請輸入正確且不超過 50 碼的 E-Mail。";
|
||||
errorMessage.value = '請輸入正確且不超過 50 碼的 E-Mail。'
|
||||
} else if (status !== 401) {
|
||||
errorMessage.value = "E-Mail 儲存失敗,請稍後再試。";
|
||||
errorMessage.value = 'E-Mail 儲存失敗,請稍後再試。'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function skipForNow() {
|
||||
if (EMAIL_COMPLETION_REQUIRED) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
await router.replace(redirectTarget());
|
||||
await router.replace(redirectTarget())
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -62,13 +58,9 @@ async function skipForNow() {
|
||||
<v-row justify="center" no-gutters>
|
||||
<v-col cols="12" sm="8" md="6">
|
||||
<v-card class="rounded-xl mt-6" color="surface">
|
||||
<v-card-title class="text-primary font-weight-bold">
|
||||
補登 E-Mail
|
||||
</v-card-title>
|
||||
<v-card-title class="text-primary font-weight-bold"> 註冊 E-Mail </v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-1 mb-5">
|
||||
設定 E-Mail,有助於日後接收帳號安全與重要服務通知。
|
||||
</p>
|
||||
<p class="text-body-1 mb-5">填寫 E-Mail,有助於日後接收帳號安全與重要服務通知。</p>
|
||||
|
||||
<EmailCompletionForm
|
||||
:error-message="errorMessage"
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import ForgotPasswordForm from '@/components/auth/ForgotPasswordForm.vue'
|
||||
import PasswordRecoveryShell from '@/components/auth/PasswordRecoveryShell.vue'
|
||||
import { useForgotPassword } from '@/composables/useForgotPassword'
|
||||
const recoverySteps = [
|
||||
{ title: '輸入已註冊手機門號', icon: 'mdi-phone' },
|
||||
{ title: '收起E-Mail,使用連結進行重設密碼', icon: 'mdi-email-outline' },
|
||||
]
|
||||
|
||||
const { accepted, loading, disabled, errorMessage, fieldErrors, remainingSeconds, submit } =
|
||||
useForgotPassword()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PasswordRecoveryShell title="忘記密碼">
|
||||
<template #description>
|
||||
<v-sheet class="pa-4 mb-6" color="background" rounded="lg">
|
||||
<ol aria-label="密碼重設流程" class="recovery-flow ma-0 pa-0">
|
||||
<li v-for="(step, index) in recoverySteps" :key="step.title" class="recovery-step">
|
||||
<v-avatar
|
||||
class="recovery-step-icon"
|
||||
color="primary"
|
||||
rounded="lg"
|
||||
size="40"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon aria-hidden="true" :icon="step.icon" size="22" />
|
||||
</v-avatar>
|
||||
<div class="recovery-step-content">
|
||||
<span class="recovery-step-number text-primary">
|
||||
步驟 {{ String(index + 1).padStart(2, '0') }}
|
||||
</span>
|
||||
<p class="text-title-small text-high-emphasis ma-0">
|
||||
{{ step.title }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</v-sheet>
|
||||
</template>
|
||||
<template v-if="accepted">
|
||||
<v-alert type="success" variant="tonal" role="status">
|
||||
如果帳號存在,系統將寄送密碼重設信件。
|
||||
</v-alert>
|
||||
<p class="mt-4 text-body-1">請查看信箱與垃圾郵件匣。</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-alert v-if="errorMessage" class="mb-4" type="error" variant="tonal" role="alert">{{
|
||||
errorMessage
|
||||
}}</v-alert>
|
||||
<ForgotPasswordForm
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:errors="fieldErrors"
|
||||
:remaining-seconds="remainingSeconds"
|
||||
@submit="submit"
|
||||
/>
|
||||
</template>
|
||||
</PasswordRecoveryShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.recovery-flow {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.recovery-step {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 40px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
}
|
||||
|
||||
.recovery-step:not(:last-child) {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.recovery-step:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
bottom: 4px;
|
||||
left: 19px;
|
||||
width: 2px;
|
||||
background: rgba(var(--v-theme-primary), 0.18);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.recovery-step-icon {
|
||||
align-self: start;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.recovery-step-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recovery-step-number {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
</style>
|
||||
@@ -198,6 +198,19 @@
|
||||
既有帳號建立密碼
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg"
|
||||
color="primary"
|
||||
prepend-icon="mdi-lock-reset"
|
||||
to="/forgot-password"
|
||||
variant="text"
|
||||
>
|
||||
忘記密碼
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<div class="d-flex align-center mt-4 px-4">
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
<v-card class="rounded-lg mt-4" color="surface">
|
||||
<v-card-title> 登入 </v-card-title>
|
||||
<v-card-text> 請輸入您的手機號碼與登入密碼 </v-card-text>
|
||||
<v-alert
|
||||
v-if="route.query.passwordReset === 'success'"
|
||||
class="mx-4 mb-4"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
>
|
||||
密碼已更新,請使用新密碼登入。
|
||||
</v-alert>
|
||||
<!-- 表單 -->
|
||||
<v-form class="pa-4" @submit.prevent="handleLogin">
|
||||
<v-alert
|
||||
@@ -91,6 +99,9 @@
|
||||
>
|
||||
既有帳號建立密碼
|
||||
</v-btn>
|
||||
<v-btn block color="primary" to="/forgot-password" variant="text">
|
||||
忘記密碼
|
||||
</v-btn>
|
||||
</v-form>
|
||||
<!-- <div class="d-flex align-center justify-center my-4">
|
||||
<v-divider class="mr-3" />
|
||||
|
||||
Reference in New Issue
Block a user