Files
opencloud/src/views/encyclopedia/EncyclopediaView.vue
T

222 lines
8.5 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 { onMounted, ref } from 'vue'
import { NAlert, NEmpty, NIcon, NSkeleton } from 'naive-ui'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
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 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()
})
</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>
</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>
</div>
</div>
<div v-else-if="encyclopediaStore.cloudTypes.length" class="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
<button
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'"
>
<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>
<div v-if="!isUnlocked(cloudType.id)" class="absolute inset-0 flex items-center justify-center bg-slate-950/12">
<span class="inline-flex items-center gap-2 border border-slate-300 bg-white/92 px-3 py-2 text-xs font-semibold text-slate-700 shadow-[3px_3px_0_0_rgba(15,23,42,0.10)] backdrop-blur">
<NIcon size="15"><Lock /></NIcon>
尚未收录
</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>
<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>
</div>
</button>
</div>
<div v-else class="oc-empty-card border border-dashed border-slate-300 bg-white px-6 py-12">
<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-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>