## 概述 本分支对前端进行了大规模适配与升级,核心是对齐新版后端 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
88 lines
3.7 KiB
Vue
88 lines
3.7 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onUnmounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { NAlert, NButton, NCard, NForm, NFormItem, NInput, NResult } from 'naive-ui'
|
||
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 timer: ReturnType<typeof setInterval> | null = null
|
||
|
||
const buttonLabel = computed(() => loading.value ? '发送中...' : cooldown.value > 0 ? `重新发送(${cooldown.value}s)` : '重新发送')
|
||
|
||
function startCooldown() {
|
||
if (timer) clearInterval(timer)
|
||
cooldown.value = 60
|
||
timer = setInterval(() => {
|
||
cooldown.value--
|
||
if (cooldown.value <= 0 && timer) {
|
||
clearInterval(timer)
|
||
timer = null
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
async function submit() {
|
||
error.value = ''
|
||
const value = email.value.trim()
|
||
if (!value || value.length > 64) {
|
||
error.value = '请输入不超过 64 个字符的有效邮箱'
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
await authStore.sendPasswordReset(value)
|
||
email.value = value
|
||
submitted.value = true
|
||
startCooldown()
|
||
} catch (e: unknown) {
|
||
error.value = e instanceof Error ? e.message : '重置邮件发送失败,请稍后重试'
|
||
} 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="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>
|
||
<div class="flex flex-wrap justify-center gap-3">
|
||
<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>
|
||
</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">输入注册邮箱,我们会发送有效期为 30 分钟的重置链接。</p>
|
||
</div>
|
||
<NForm @submit.prevent="submit">
|
||
<NFormItem label="邮箱">
|
||
<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>
|
||
</NForm>
|
||
<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>
|
||
</div>
|
||
</template>
|