Files
opencloud/src/views/auth/ForgotPasswordView.vue
T
Mplan 720ad29f06 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
2026-08-22 21:39:42 +08:00

88 lines
3.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>