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:
@@ -1,109 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { NAlert, NEmpty, NIcon, NSkeleton } from 'naive-ui'
|
||||
import { onMounted } from 'vue'
|
||||
import { NEmpty, NSkeleton } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { imageUrl } from '@/lib/api'
|
||||
import { useEncyclopediaStore } from '@/stores/encyclopedia'
|
||||
import EncyclopediaProgressCard from '@/components/cloud/EncyclopediaProgressCard.vue'
|
||||
import { Lock } from '@vicons/tabler'
|
||||
import type { CloudType } from '@/types/database'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const encyclopediaStore = useEncyclopediaStore()
|
||||
|
||||
const lockHint = ref('')
|
||||
let lockHintTimer: number | null = null
|
||||
|
||||
const rarityMeta = {
|
||||
common: { label: '常见', chip: 'bg-sky-100 text-sky-700 border-sky-200', glow: 'from-sky-100 to-white' },
|
||||
uncommon: { label: '少见', chip: 'bg-amber-100 text-amber-700 border-amber-200', glow: 'from-amber-100 to-white' },
|
||||
rare: { label: '罕见', chip: 'bg-rose-100 text-rose-700 border-rose-200', glow: 'from-rose-100 to-white' },
|
||||
} satisfies Record<CloudType['rarity'], { label: string; chip: string; glow: string }>
|
||||
|
||||
function isUnlocked(cloudTypeId: number) {
|
||||
return authStore.isLoggedIn && encyclopediaStore.isUnlocked(cloudTypeId)
|
||||
function typeImageUrl(iconId: string | null) {
|
||||
return iconId ? imageUrl(iconId, 'preview') : ''
|
||||
}
|
||||
|
||||
function getCollectionEntry(cloudTypeId: number) {
|
||||
if (!authStore.isLoggedIn) return null
|
||||
return encyclopediaStore.getCollectionEntry(cloudTypeId)
|
||||
}
|
||||
|
||||
function formatUnlockedAt(iso: string) {
|
||||
return new Date(iso).toLocaleDateString('zh-CN', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
function showLockHint(name: string) {
|
||||
lockHint.value = `拍到 ${name} 后,这枚徽章就会亮起来。`
|
||||
if (lockHintTimer) window.clearTimeout(lockHintTimer)
|
||||
lockHintTimer = window.setTimeout(() => {
|
||||
lockHint.value = ''
|
||||
lockHintTimer = null
|
||||
}, 2200)
|
||||
}
|
||||
|
||||
function openCard(cloudType: CloudType) {
|
||||
if (isUnlocked(cloudType.id)) {
|
||||
router.push(`/encyclopedia/${cloudType.id}`)
|
||||
return
|
||||
}
|
||||
showLockHint(cloudType.name)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await encyclopediaStore.fetchCloudTypes()
|
||||
await encyclopediaStore.fetchMyCollection()
|
||||
})
|
||||
onMounted(() => encyclopediaStore.fetchCloudTypes())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="oc-page-hero">
|
||||
<div class="oc-container oc-container--content oc-page-hero__inner">
|
||||
<div class="oc-page-hero__split">
|
||||
<div class="min-w-0">
|
||||
<p class="oc-page-eyebrow">Cloud Encyclopedia</p>
|
||||
<h1 class="oc-page-title">云朵图鉴</h1>
|
||||
<p class="oc-page-description">
|
||||
收集 10 种基础云属。每拍到一种新的云型,图鉴里就会点亮一枚徽章,并记录你第一次遇见它的时间。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<EncyclopediaProgressCard
|
||||
:unlocked-count="authStore.isLoggedIn ? encyclopediaStore.unlockedCount : 0"
|
||||
:total-count="encyclopediaStore.cloudTypes.length || 10"
|
||||
:is-logged-in="authStore.isLoggedIn"
|
||||
/>
|
||||
</div>
|
||||
<p class="oc-page-eyebrow">Cloud Encyclopedia</p>
|
||||
<h1 class="oc-page-title">云朵百科</h1>
|
||||
<p class="oc-page-description">
|
||||
浏览后端提供的云类型资料,了解云属名称、识别说明和社区公开观测记录。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oc-container oc-container--content oc-page-content">
|
||||
<NAlert
|
||||
v-if="encyclopediaStore.collectionError && authStore.isLoggedIn"
|
||||
class="mb-6"
|
||||
type="warning"
|
||||
:show-icon="false"
|
||||
:bordered="false"
|
||||
title="图鉴收藏数据暂时不可用"
|
||||
>
|
||||
图鉴收藏数据暂时不可用:{{ encyclopediaStore.collectionError }}
|
||||
</NAlert>
|
||||
|
||||
<div v-if="encyclopediaStore.loadingCloudTypes" class="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div v-for="n in 6" :key="n" class="oc-panel-card overflow-hidden border border-slate-200 bg-white">
|
||||
<NSkeleton class="h-44 w-full border-b border-slate-200" />
|
||||
<div class="p-5">
|
||||
<NSkeleton class="h-3 w-1/3" />
|
||||
<NSkeleton class="mt-3 h-6 w-2/5" />
|
||||
<NSkeleton class="mt-2 h-3 w-1/2" />
|
||||
<NSkeleton class="mt-5 h-4 w-full" :repeat="2" />
|
||||
<NSkeleton class="mt-5 h-10 w-full" />
|
||||
</div>
|
||||
<NSkeleton class="h-80 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,114 +39,54 @@ onMounted(async () => {
|
||||
v-for="cloudType in encyclopediaStore.cloudTypes"
|
||||
:key="cloudType.id"
|
||||
type="button"
|
||||
@click="openCard(cloudType)"
|
||||
class="encyclopedia-type-card oc-interactive-card oc-panel-card flex h-full flex-col overflow-hidden border bg-white text-left"
|
||||
:class="isUnlocked(cloudType.id) ? 'border-sky-200' : 'border-slate-200'"
|
||||
class="oc-interactive-card oc-panel-card relative isolate min-h-80 overflow-hidden border border-slate-200 bg-sky-50 text-left"
|
||||
@click="router.push(`/encyclopedia/${cloudType.id}`)"
|
||||
>
|
||||
<div class="relative h-44 shrink-0 overflow-hidden border-b border-slate-200 bg-sky-50">
|
||||
<img
|
||||
v-if="getCollectionEntry(cloudType.id)?.firstCloud?.image_url"
|
||||
:src="getCollectionEntry(cloudType.id)?.firstCloud?.thumbnail_url || getCollectionEntry(cloudType.id)?.firstCloud?.image_url || ''"
|
||||
:alt="cloudType.name"
|
||||
class="encyclopedia-type-card__image h-full w-full object-cover"
|
||||
:class="isUnlocked(cloudType.id) ? '' : 'grayscale contrast-75'"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center bg-gradient-to-br"
|
||||
:class="isUnlocked(cloudType.id) ? rarityMeta[cloudType.rarity].glow : 'from-slate-200 to-slate-50'"
|
||||
>
|
||||
<span class="font-mono text-5xl font-bold text-slate-500/75">{{ cloudType.name.slice(0, 1) }}</span>
|
||||
</div>
|
||||
<img
|
||||
v-if="cloudType.icon_id"
|
||||
:src="typeImageUrl(cloudType.icon_id)"
|
||||
:alt="`${cloudType.name}介绍图`"
|
||||
class="absolute inset-0 -z-20 h-full w-full object-cover"
|
||||
/>
|
||||
<div
|
||||
v-if="cloudType.icon_id"
|
||||
class="absolute inset-0 -z-10 bg-gradient-to-t from-slate-950/90 via-slate-950/55 to-slate-950/25"
|
||||
></div>
|
||||
<div v-else class="absolute inset-0 -z-10 flex items-center justify-center text-sm font-medium text-slate-500">无图片</div>
|
||||
|
||||
<div v-if="!isUnlocked(cloudType.id)" class="absolute inset-0 flex items-center justify-center bg-slate-950/12">
|
||||
<span class="encyclopedia-lock-badge inline-flex items-center gap-2 border border-slate-300 bg-white/92 px-3 py-2 text-xs font-semibold text-slate-700 backdrop-blur">
|
||||
<NIcon size="15"><Lock /></NIcon>
|
||||
尚未收录
|
||||
<div class="flex min-h-80 flex-col p-5">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="oc-section-label" :class="cloudType.icon_id ? 'text-white/70' : 'text-slate-500'">Cloud Genus</p>
|
||||
<h2 class="mt-2 text-xl font-bold" :class="cloudType.icon_id ? 'text-white' : 'text-slate-900'">{{ cloudType.name }}</h2>
|
||||
<p
|
||||
class="mt-1 font-mono text-xs uppercase tracking-[0.12em]"
|
||||
:class="cloudType.icon_id ? 'text-white/75' : 'text-slate-500'"
|
||||
>
|
||||
{{ cloudType.genus || '未提供云属名称' }}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="shrink-0 border px-2.5 py-1 text-xs font-medium"
|
||||
:class="cloudType.icon_id ? 'border-white/35 bg-slate-950/25 text-white backdrop-blur' : 'border-slate-200 bg-white text-slate-600'"
|
||||
>
|
||||
稀有度 {{ cloudType.rarity_value }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute left-4 top-4">
|
||||
<span class="inline-flex border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.12em]" :class="rarityMeta[cloudType.rarity].chip">
|
||||
{{ rarityMeta[cloudType.rarity].label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 flex-col p-5">
|
||||
<p class="oc-section-label">Cloud Genus</p>
|
||||
<h2 class="mt-2 text-xl font-bold text-slate-900">{{ cloudType.name }}</h2>
|
||||
<p class="mt-1 font-mono text-xs uppercase tracking-[0.12em] text-slate-500">{{ cloudType.name_en }}</p>
|
||||
|
||||
<p class="mt-4 line-clamp-2 min-h-[3.25rem] text-sm leading-6 text-slate-600">
|
||||
{{ cloudType.description || '云层特征和识别要点会显示在这里。' }}
|
||||
<p class="mt-auto line-clamp-3 pt-16 text-sm leading-6" :class="cloudType.icon_id ? 'text-white/85' : 'text-slate-600'">
|
||||
{{ cloudType.description || '暂未提供识别说明。' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between gap-3 border-t border-slate-200 pt-4 text-xs">
|
||||
<template v-if="isUnlocked(cloudType.id) && getCollectionEntry(cloudType.id)">
|
||||
<span class="font-semibold uppercase tracking-[0.12em] text-sky-800">已收录</span>
|
||||
<span class="text-right text-slate-500">首次记录 {{ formatUnlockedAt(getCollectionEntry(cloudType.id)!.unlocked_at) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="font-semibold uppercase tracking-[0.12em] text-slate-600">等待发现</span>
|
||||
<span class="text-right text-slate-400">拍到后点亮</span>
|
||||
</template>
|
||||
</div>
|
||||
<p
|
||||
class="mt-4 border-t pt-4 text-xs font-semibold"
|
||||
:class="cloudType.icon_id ? 'border-white/25 text-white' : 'border-slate-200 text-sky-700'"
|
||||
>查看类型资料与公开云图 →</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="oc-empty-card border border-dashed border-slate-300 bg-white px-6 py-12">
|
||||
<NEmpty description="暂时还没有图鉴数据" />
|
||||
<NEmpty description="暂时还没有云类型资料" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="lockHint"
|
||||
class="encyclopedia-lock-hint fixed bottom-6 left-1/2 z-50 -translate-x-1/2 border border-slate-900 bg-slate-900 px-4 py-2 text-sm text-white"
|
||||
>
|
||||
{{ lockHint }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.encyclopedia-lock-badge {
|
||||
box-shadow: var(--oc-shadow-xs);
|
||||
}
|
||||
|
||||
.encyclopedia-lock-hint {
|
||||
box-shadow: var(--oc-shadow-lg);
|
||||
}
|
||||
|
||||
.encyclopedia-type-card:hover {
|
||||
border-color: rgb(125 211 252);
|
||||
box-shadow: var(--oc-shadow-lg) !important;
|
||||
transform: translate(-1px, -1px);
|
||||
}
|
||||
|
||||
.encyclopedia-type-card:active {
|
||||
box-shadow: var(--oc-shadow-md) !important;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.encyclopedia-type-card__image {
|
||||
transition: transform 500ms ease;
|
||||
}
|
||||
|
||||
.encyclopedia-type-card:hover .encyclopedia-type-card__image {
|
||||
transform: scale(1.035);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.encyclopedia-type-card,
|
||||
.encyclopedia-type-card__image {
|
||||
transition-duration: 1ms;
|
||||
}
|
||||
|
||||
.encyclopedia-type-card:hover,
|
||||
.encyclopedia-type-card:hover .encyclopedia-type-card__image {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user