更新ui样式,并修复已知问题 #8
@@ -1,61 +1,55 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { NCard, NProgress } from 'naive-ui'
|
import { NCard } from 'naive-ui'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
unlockedCount: number
|
unlockedCount: number
|
||||||
totalCount: number
|
totalCount: number
|
||||||
percent: number
|
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function lerpHex(a: string, b: string, t: number) {
|
const displayedUnlockedCount = computed(() => {
|
||||||
const ah = parseInt(a.slice(1), 16)
|
if (!props.isLoggedIn) return 0
|
||||||
const bh = parseInt(b.slice(1), 16)
|
return Math.min(Math.max(props.unlockedCount, 0), props.totalCount)
|
||||||
const r = Math.round(((ah >> 16) & 0xff) + (((bh >> 16) & 0xff) - ((ah >> 16) & 0xff)) * t)
|
})
|
||||||
const g = Math.round(((ah >> 8) & 0xff) + (((bh >> 8) & 0xff) - ((ah >> 8) & 0xff)) * t)
|
const remainingCount = computed(() => Math.max(props.totalCount - displayedUnlockedCount.value, 0))
|
||||||
const bv = Math.round((ah & 0xff) + ((bh & 0xff) - (ah & 0xff)) * t)
|
|
||||||
return `#${((r << 16) | (g << 8) | bv).toString(16).padStart(6, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function progressColor(pct: number) {
|
|
||||||
const t = Math.min(1, Math.max(0, pct / 100))
|
|
||||||
if (t <= 0.5) return lerpHex('#0ea5e9', '#f59e0b', t * 2)
|
|
||||||
return lerpHex('#f59e0b', '#ef4444', (t - 0.5) * 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const progressText = computed(() => `${props.unlockedCount}/${props.totalCount}`)
|
|
||||||
const currentColor = computed(() => progressColor(props.isLoggedIn ? props.percent : 0))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NCard class="oc-panel-card-soft border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
<NCard class="oc-panel-card-soft border border-sky-100 bg-white/88 backdrop-blur" :bordered="false">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-slate-500">当前进度</p>
|
<p class="oc-stat-label">Collection Status</p>
|
||||||
<p class="mt-1 text-3xl font-bold" :style="{ color: currentColor }">{{ progressText }}</p>
|
<p class="mt-1 text-base font-semibold text-slate-900">图鉴收集进度</p>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex h-16 w-16 items-center justify-center border text-xl font-semibold text-white transition-colors duration-500"
|
|
||||||
:style="{ backgroundColor: currentColor, borderColor: currentColor }"
|
|
||||||
>
|
|
||||||
{{ isLoggedIn ? percent : 0 }}%
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="shrink-0 font-mono tabular-nums text-slate-500">
|
||||||
|
<span class="text-3xl font-bold text-sky-800">{{ displayedUnlockedCount }}</span>
|
||||||
|
<span class="ml-1 text-sm">/ {{ totalCount }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NProgress
|
<div
|
||||||
class="mt-5"
|
class="mt-5 grid grid-cols-5 gap-2 sm:grid-cols-10"
|
||||||
type="line"
|
role="progressbar"
|
||||||
:show-indicator="false"
|
aria-label="云属收集进度"
|
||||||
:percentage="isLoggedIn ? percent : 0"
|
aria-valuemin="0"
|
||||||
:color="{ stops: ['#0ea5e9', currentColor] }"
|
:aria-valuemax="totalCount"
|
||||||
:height="12"
|
:aria-valuenow="displayedUnlockedCount"
|
||||||
rail-color="#e2e8f0"
|
:aria-valuetext="`已收录 ${displayedUnlockedCount} 种,共 ${totalCount} 种`"
|
||||||
/>
|
>
|
||||||
|
<span
|
||||||
|
v-for="index in totalCount"
|
||||||
|
:key="index"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="encyclopedia-progress-cell aspect-square border border-slate-300 bg-slate-50"
|
||||||
|
:class="{ 'encyclopedia-progress-cell--active': index <= displayedUnlockedCount }"
|
||||||
|
:style="{ animationDelay: `${(index - 1) * 90}ms` }"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="mt-4 text-sm text-slate-500">
|
<p class="mt-4 border-t border-slate-200 pt-3 text-sm text-slate-500">
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
已解锁 {{ unlockedCount }} 枚徽章,还差 {{ Math.max(totalCount - unlockedCount, 0) }} 枚。
|
已收录 {{ displayedUnlockedCount }} 种云属,还差 {{ remainingCount }} 种。
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
登录后可同步你的个人图鉴进度。
|
登录后可同步你的个人图鉴进度。
|
||||||
@@ -63,3 +57,52 @@ const currentColor = computed(() => progressColor(props.isLoggedIn ? props.perce
|
|||||||
</p>
|
</p>
|
||||||
</NCard>
|
</NCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.encyclopedia-progress-cell--active {
|
||||||
|
background: rgb(224 242 254);
|
||||||
|
border-color: rgb(125 211 252);
|
||||||
|
box-shadow: 3px 3px 0 0 rgb(3 105 161 / 14%);
|
||||||
|
animation:
|
||||||
|
encyclopedia-cell-activate 1ms linear both,
|
||||||
|
encyclopedia-cell-pulse 220ms ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes encyclopedia-cell-activate {
|
||||||
|
from {
|
||||||
|
background: rgb(248 250 252);
|
||||||
|
border-color: rgb(203 213 225);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
background: rgb(224 242 254);
|
||||||
|
border-color: rgb(125 211 252);
|
||||||
|
box-shadow: 3px 3px 0 0 rgb(3 105 161 / 14%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes encyclopedia-cell-pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
28% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
62% {
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.encyclopedia-progress-cell--active {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -364,26 +364,51 @@ async function handleLogout() {
|
|||||||
>
|
>
|
||||||
图鉴
|
图鉴
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink
|
|
||||||
v-if="!authStore.loading && authStore.isLoggedIn"
|
|
||||||
to="/profile"
|
|
||||||
class="oc-nav-link shrink-0"
|
|
||||||
:class="route.name === 'profile' ? activeNavClass : inactiveNavClass"
|
|
||||||
>
|
|
||||||
主页
|
|
||||||
</RouterLink>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<button
|
<Transition name="header-reveal">
|
||||||
v-if="headerHidden"
|
<div
|
||||||
type="button"
|
v-if="headerHidden"
|
||||||
class="absolute left-1/2 top-3 flex -translate-x-1/2 items-center gap-2 border border-slate-300 bg-white/94 px-3 py-1.5 text-xs font-medium tracking-[0.16em] text-slate-700 shadow-[4px_4px_0_0_rgba(15,23,42,0.08)] backdrop-blur transition-colors hover:border-teal-300 hover:bg-teal-50 hover:text-teal-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600"
|
class="pointer-events-none absolute inset-x-0 top-3 flex justify-center"
|
||||||
@click="showHeader(true)"
|
>
|
||||||
>
|
<button
|
||||||
<span>▼</span>
|
type="button"
|
||||||
<span>展开导航</span>
|
class="pointer-events-auto flex items-center gap-2 border border-slate-300 bg-white/94 px-3 py-1.5 text-xs font-medium tracking-[0.16em] text-slate-700 shadow-[4px_4px_0_0_rgba(15,23,42,0.08)] backdrop-blur transition-colors hover:border-teal-300 hover:bg-teal-50 hover:text-teal-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600"
|
||||||
</button>
|
@click="showHeader(true)"
|
||||||
|
>
|
||||||
|
<span>▼</span>
|
||||||
|
<span>展开导航</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-reveal-enter-active,
|
||||||
|
.header-reveal-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 240ms ease,
|
||||||
|
transform 240ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-reveal-enter-from,
|
||||||
|
.header-reveal-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-0.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.header-reveal-enter-active,
|
||||||
|
.header-reveal-leave-active {
|
||||||
|
transition-duration: 1ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-reveal-enter-from,
|
||||||
|
.header-reveal-leave-to {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ onMounted(async () => {
|
|||||||
<EncyclopediaProgressCard
|
<EncyclopediaProgressCard
|
||||||
:unlocked-count="authStore.isLoggedIn ? encyclopediaStore.unlockedCount : 0"
|
:unlocked-count="authStore.isLoggedIn ? encyclopediaStore.unlockedCount : 0"
|
||||||
:total-count="encyclopediaStore.cloudTypes.length || 10"
|
:total-count="encyclopediaStore.cloudTypes.length || 10"
|
||||||
:percent="encyclopediaStore.unlockPercent"
|
|
||||||
:is-logged-in="authStore.isLoggedIn"
|
:is-logged-in="authStore.isLoggedIn"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +94,7 @@ onMounted(async () => {
|
|||||||
图鉴收藏数据暂时不可用:{{ encyclopediaStore.collectionError }}
|
图鉴收藏数据暂时不可用:{{ encyclopediaStore.collectionError }}
|
||||||
</NAlert>
|
</NAlert>
|
||||||
|
|
||||||
<div v-if="encyclopediaStore.loadingCloudTypes" class="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
<div v-if="encyclopediaStore.loadingCloudTypes" class="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<NCard v-for="n in 6" :key="n" class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
<NCard v-for="n in 6" :key="n" class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||||
<NSkeleton class="h-44 w-full" />
|
<NSkeleton class="h-44 w-full" />
|
||||||
<NSkeleton class="mt-5 h-5 w-2/5" />
|
<NSkeleton class="mt-5 h-5 w-2/5" />
|
||||||
@@ -104,7 +103,7 @@ onMounted(async () => {
|
|||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="encyclopediaStore.cloudTypes.length" class="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
<div v-else-if="encyclopediaStore.cloudTypes.length" class="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<button
|
<button
|
||||||
v-for="cloudType in encyclopediaStore.cloudTypes"
|
v-for="cloudType in encyclopediaStore.cloudTypes"
|
||||||
:key="cloudType.id"
|
:key="cloudType.id"
|
||||||
|
|||||||
Reference in New Issue
Block a user