feat: 适配新版后端 API 并接入邮箱验证 (#9)
## 概述 本分支对前端进行了大规模适配与升级,核心是对齐新版后端 API 接口,并接入邮箱验证与密码找回流程。 ## 主要变更 - **API 适配**:对齐新版后端接口契约,重构 api 客户端与类型定义,移除前端臆造字段 - **认证流程**:接入邮箱验证(确认 / 重发确认)、密码找回(忘记密码 / 重置密码)全流程 - **管理后台**:前端对齐更新后的 API 契约 - **云图鉴**:展示云类型图标图片 - **UI 优化**:图片预览改为直角样式并调整关闭按钮 ## 影响范围 - API 层:src/lib/api.ts、src/types/api.ts、src/types/database.ts - 认证视图:src/views/auth/* - 管理后台:src/views/admin/AdminView.vue - 图鉴视图:src/views/encyclopedia/* - 状态管理:src/stores/* Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
@@ -6,123 +6,61 @@ import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const route = useRoute()
|
||||
|
||||
const email = ref(typeof route.query.email === 'string' ? route.query.email : '')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const submitted = ref(false)
|
||||
const cooldown = ref(0)
|
||||
let cooldownTimer: ReturnType<typeof setInterval> | null = null
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const resendButtonLabel = computed(() => {
|
||||
if (loading.value) return '发送中...'
|
||||
if (cooldown.value > 0) return `重新发送(${cooldown.value}s)`
|
||||
return '重新发送'
|
||||
})
|
||||
|
||||
function clearCooldownTimer() {
|
||||
if (!cooldownTimer) return
|
||||
clearInterval(cooldownTimer)
|
||||
cooldownTimer = null
|
||||
}
|
||||
const buttonLabel = computed(() => loading.value ? '发送中...' : cooldown.value > 0 ? `重新发送(${cooldown.value}s)` : '重新发送')
|
||||
|
||||
function startCooldown() {
|
||||
clearCooldownTimer()
|
||||
if (timer) clearInterval(timer)
|
||||
cooldown.value = 60
|
||||
cooldownTimer = setInterval(() => {
|
||||
timer = setInterval(() => {
|
||||
cooldown.value--
|
||||
if (cooldown.value <= 0) {
|
||||
cooldown.value = 0
|
||||
clearCooldownTimer()
|
||||
if (cooldown.value <= 0 && timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function handleSendResetEmail() {
|
||||
async function submit() {
|
||||
error.value = ''
|
||||
|
||||
if (!email.value.trim()) {
|
||||
error.value = '请输入需要找回密码的邮箱。'
|
||||
const value = email.value.trim()
|
||||
if (!value || value.length > 64) {
|
||||
error.value = '请输入不超过 64 个字符的有效邮箱'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await authStore.sendPasswordReset(email.value.trim())
|
||||
await authStore.sendPasswordReset(value)
|
||||
email.value = value
|
||||
submitted.value = true
|
||||
startCooldown()
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : '重置邮件发送失败,请稍后重试。'
|
||||
error.value = e instanceof Error ? e.message : '重置邮件发送失败,请稍后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function resendResetEmail() {
|
||||
if (cooldown.value > 0 || loading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await handleSendResetEmail()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearCooldownTimer()
|
||||
})
|
||||
onUnmounted(() => { if (timer) clearInterval(timer) })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
|
||||
<div class="mx-auto grid min-w-0 max-w-6xl gap-8 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
|
||||
<section class="oc-panel-card-xl min-w-0 overflow-hidden border border-slate-200 bg-[linear-gradient(135deg,#eff6ff_0%,#ffffff_52%,#f2faf6_100%)] p-6 sm:p-8">
|
||||
<p class="text-sm uppercase tracking-[0.26em] text-sky-700">Password Recovery</p>
|
||||
<h1 class="mt-4 max-w-xl break-words text-4xl font-black leading-[1.05] text-slate-900 sm:text-5xl">
|
||||
重新拿回你的
|
||||
<span class="block text-sky-700">OpenCloud 账号</span>
|
||||
</h1>
|
||||
<p class="mt-6 max-w-lg text-base leading-8 text-slate-600">
|
||||
输入注册邮箱后,我们会发送一封密码重置邮件。打开邮件中的链接,即可进入新的密码设置页面。
|
||||
</p>
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="oc-stat-label">Step 1</div>
|
||||
<div class="mt-2 text-lg font-bold text-slate-900">发送邮件</div>
|
||||
<div class="oc-field-help">使用注册邮箱请求重置链接</div>
|
||||
</div>
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="oc-stat-label">Step 2</div>
|
||||
<div class="mt-2 text-lg font-bold text-slate-900">设置新密码</div>
|
||||
<div class="oc-field-help">在专用页面完成新密码更新</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<NCard class="oc-panel-card-xl min-w-0">
|
||||
<NResult
|
||||
v-if="submitted"
|
||||
status="success"
|
||||
title="重置邮件已发送"
|
||||
description="如果该邮箱已注册,请查收邮件中的重置链接。"
|
||||
>
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<NCard class="oc-panel-card-xl">
|
||||
<NResult v-if="submitted" status="success" title="请求已受理" description="如果该邮箱符合条件,我们会发送密码重置邮件。">
|
||||
<template #footer>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-slate-500">
|
||||
目标邮箱:
|
||||
<span class="font-semibold text-slate-700">{{ email }}</span>
|
||||
</p>
|
||||
<p class="text-sm text-slate-500">目标邮箱:<span class="font-semibold text-slate-700">{{ email }}</span></p>
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
<NButton
|
||||
type="primary"
|
||||
class="oc-primary-button oc-primary-button--teal"
|
||||
:disabled="cooldown > 0"
|
||||
:loading="loading"
|
||||
@click="resendResetEmail"
|
||||
>
|
||||
{{ resendButtonLabel }}
|
||||
</NButton>
|
||||
<RouterLink :to="{ path: '/login', query: { email } }" class="no-underline">
|
||||
<NButton type="default" secondary strong class="oc-panel-button oc-panel-button--neutral">返回登录</NButton>
|
||||
</RouterLink>
|
||||
<NButton type="default" class="oc-panel-button oc-panel-button--sky" :disabled="cooldown > 0" :loading="loading" @click="submit">{{ buttonLabel }}</NButton>
|
||||
<RouterLink :to="{ path: '/login', query: { email } }" class="no-underline"><NButton type="default" class="oc-panel-button oc-panel-button--neutral">返回登录</NButton></RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -130,48 +68,18 @@ onUnmounted(() => {
|
||||
|
||||
<template v-else>
|
||||
<div class="mb-8">
|
||||
<div class="text-sm uppercase tracking-[0.22em] text-slate-500">Reset Request</div>
|
||||
<h2 class="mt-3 text-3xl font-bold text-slate-900">忘记密码</h2>
|
||||
<p class="mt-2 text-sm text-slate-500">输入你的注册邮箱,我们会发送一封密码重置邮件。</p>
|
||||
<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">输入注册邮箱,我们会发送有效期为 30 分钟的重置链接。</p>
|
||||
</div>
|
||||
|
||||
<NForm @submit.prevent="handleSendResetEmail">
|
||||
<NForm @submit.prevent="submit">
|
||||
<NFormItem label="邮箱">
|
||||
<NInput
|
||||
v-model:value="email"
|
||||
type="text"
|
||||
:input-props="{
|
||||
autocomplete: 'email',
|
||||
inputmode: 'email',
|
||||
name: 'email',
|
||||
required: true,
|
||||
}"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
<NInput v-model:value="email" maxlength="64" type="text" placeholder="your@email.com" :input-props="{ autocomplete: 'email', inputmode: 'email', name: 'email', required: true }" />
|
||||
</NFormItem>
|
||||
|
||||
<NAlert v-if="error" type="error" class="mb-4">
|
||||
{{ error }}
|
||||
</NAlert>
|
||||
|
||||
<NButton
|
||||
attr-type="submit"
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
class="oc-primary-button oc-primary-button--sky"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ loading ? '发送中...' : '发送重置邮件' }}
|
||||
</NButton>
|
||||
<NAlert v-if="error" type="error" class="mb-4">{{ error }}</NAlert>
|
||||
<NButton attr-type="submit" type="primary" block size="large" class="oc-primary-button oc-primary-button--sky" :loading="loading">{{ loading ? '发送中...' : '发送重置邮件' }}</NButton>
|
||||
</NForm>
|
||||
|
||||
<div class="mt-6 flex flex-wrap items-center justify-between gap-3 text-sm text-slate-500">
|
||||
<p>如果你已经记起密码,可以直接返回登录。</p>
|
||||
<RouterLink :to="{ path: '/login', query: { email } }" class="font-semibold text-sky-700 hover:text-sky-800">
|
||||
返回登录
|
||||
</RouterLink>
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-slate-500"><RouterLink :to="{ path: '/login', query: { email } }" class="font-semibold text-sky-700 hover:text-sky-800">返回登录</RouterLink></p>
|
||||
</template>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user