重组前端功能目录并统一代码规范工具链 (#10)

Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
2026-09-29 18:09:38 +08:00
parent 720ad29f06
commit 0af0256c97
101 changed files with 14644 additions and 10309 deletions
@@ -0,0 +1,153 @@
<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>