154 lines
6.1 KiB
Vue
154 lines
6.1 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onUnmounted, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { NAlert, NButton, NCard, NForm, NFormItem, NInput, NResult } from 'naive-ui'
|
||
import { AuthClientError } from '@/features/auth/authClient'
|
||
import { useAuthStore } from '@/features/auth/stores/auth'
|
||
|
||
const authStore = useAuthStore()
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const email = typeof route.query.email === 'string' ? route.query.email : ''
|
||
const token = typeof route.query.token === 'string' ? route.query.token : ''
|
||
const password = ref('')
|
||
const confirmPassword = ref('')
|
||
const error = ref(!token ? '密码重置链接缺少必要信息,请重新发送邮件。' : '')
|
||
const invalid = ref(!token)
|
||
const success = ref(false)
|
||
const loading = ref(false)
|
||
const countdown = ref(5)
|
||
let timer: ReturnType<typeof setInterval> | null = null
|
||
|
||
if (route.query.token !== undefined || route.query.email !== undefined) {
|
||
window.history.replaceState(window.history.state, '', route.path)
|
||
}
|
||
|
||
const canSubmit = computed(
|
||
() => !invalid.value && password.value.length >= 8 && password.value === confirmPassword.value,
|
||
)
|
||
|
||
function startCountdown() {
|
||
timer = setInterval(() => {
|
||
countdown.value--
|
||
if (countdown.value <= 0) {
|
||
if (timer) clearInterval(timer)
|
||
void router.push({ path: '/login', query: { email } })
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
async function submit() {
|
||
error.value = ''
|
||
if (password.value.length < 8) {
|
||
error.value = '新密码至少需要 8 位'
|
||
return
|
||
}
|
||
if (password.value !== confirmPassword.value) {
|
||
error.value = '两次输入的密码不一致'
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
await authStore.resetPassword(token, password.value)
|
||
success.value = true
|
||
startCountdown()
|
||
} catch (e: unknown) {
|
||
error.value = e instanceof Error ? e.message : '密码重置失败,请稍后重试'
|
||
if (e instanceof AuthClientError && (e.status === 400 || e.code === 'INVALID_TOKEN'))
|
||
invalid.value = true
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
if (timer) clearInterval(timer)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
|
||
<div class="mx-auto max-w-2xl">
|
||
<NCard class="oc-panel-card-xl">
|
||
<NResult
|
||
v-if="success"
|
||
status="success"
|
||
title="密码已重置"
|
||
description="请使用新密码重新登录。"
|
||
>
|
||
<template #footer>
|
||
<div class="space-y-4">
|
||
<p class="text-sm text-slate-500">
|
||
{{ countdown }} 秒后自动前往登录页面
|
||
</p>
|
||
<NButton
|
||
type="default"
|
||
class="oc-panel-button oc-panel-button--teal"
|
||
@click="router.push({ path: '/login', query: { email } })"
|
||
>立即登录</NButton
|
||
>
|
||
</div>
|
||
</template>
|
||
</NResult>
|
||
|
||
<template v-else>
|
||
<div class="mb-8">
|
||
<div class="text-sm uppercase tracking-[0.22em] text-slate-500">
|
||
Password Recovery
|
||
</div>
|
||
<h1 class="mt-3 text-3xl font-bold text-slate-900">设置新密码</h1>
|
||
<p class="mt-2 text-sm text-slate-500">
|
||
提交成功后,此重置链接和现有登录会话都会失效。
|
||
</p>
|
||
</div>
|
||
<NForm @submit.prevent="submit">
|
||
<NFormItem label="新密码"
|
||
><NInput
|
||
v-model:value="password"
|
||
type="password"
|
||
show-password-on="click"
|
||
placeholder="至少 8 位"
|
||
:disabled="invalid"
|
||
:input-props="{
|
||
autocomplete: 'new-password',
|
||
name: 'new-password',
|
||
required: true,
|
||
}"
|
||
/></NFormItem>
|
||
<NFormItem label="确认新密码"
|
||
><NInput
|
||
v-model:value="confirmPassword"
|
||
type="password"
|
||
show-password-on="click"
|
||
placeholder="再次输入新密码"
|
||
:disabled="invalid"
|
||
:input-props="{
|
||
autocomplete: 'new-password',
|
||
name: 'confirm-new-password',
|
||
required: true,
|
||
}"
|
||
/></NFormItem>
|
||
<NAlert v-if="error" type="error" class="mb-4">{{ error }}</NAlert>
|
||
<RouterLink
|
||
v-if="invalid"
|
||
:to="{ path: '/forgot-password', query: { email } }"
|
||
class="mb-4 block text-sm font-semibold text-teal-700 hover:text-teal-800"
|
||
>重新发送密码重置邮件</RouterLink
|
||
>
|
||
<NButton
|
||
attr-type="submit"
|
||
type="primary"
|
||
block
|
||
size="large"
|
||
class="oc-primary-button oc-primary-button--teal"
|
||
:disabled="!canSubmit"
|
||
:loading="loading"
|
||
>保存新密码</NButton
|
||
>
|
||
</NForm>
|
||
</template>
|
||
</NCard>
|
||
</div>
|
||
</div>
|
||
</template>
|