refactor: extract EncyclopediaProgressCard component and limit map pitch

- Create EncyclopediaProgressCard with dynamic progress color, reuse in
  encyclopedia and profile pages to unify styling
- Add skyColor and maxPitch to AMap 3D view to reduce empty tile area
  when tilting
- Extend AMap type declarations for skyColor, maxPitch, getPitch, getRotation
This commit is contained in:
2026-05-24 17:43:25 +08:00
parent 1e0da1fe36
commit 2a9f8d6a9c
5 changed files with 93 additions and 82 deletions
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { computed } from 'vue'
import { NCard, NProgress } from 'naive-ui'
const props = defineProps<{
unlockedCount: number
totalCount: number
percent: number
isLoggedIn: boolean
}>()
function lerpHex(a: string, b: string, t: number) {
const ah = parseInt(a.slice(1), 16)
const bh = parseInt(b.slice(1), 16)
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 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>
<template>
<NCard class="border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-slate-500">当前进度</p>
<p class="mt-1 text-3xl font-bold" :style="{ color: currentColor }">{{ progressText }}</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>
<NProgress
class="mt-5"
type="line"
:show-indicator="false"
:percentage="isLoggedIn ? percent : 0"
:color="{ stops: ['#0ea5e9', currentColor] }"
:height="12"
rail-color="#e2e8f0"
/>
<p class="mt-4 text-sm text-slate-500">
<template v-if="isLoggedIn">
已解锁 {{ unlockedCount }} 枚徽章还差 {{ Math.max(totalCount - unlockedCount, 0) }}
</template>
<template v-else>
登录后可同步你的个人图鉴进度
</template>
</p>
</NCard>
</template>
+4
View File
@@ -8,7 +8,9 @@ declare namespace AMap {
setCenter(center: [number, number]): void setCenter(center: [number, number]): void
setZoom(zoom: number): void setZoom(zoom: number): void
setPitch(pitch: number): void setPitch(pitch: number): void
getPitch(): number
setRotation(rotation: number): void setRotation(rotation: number): void
getRotation(): number
on(event: string, callback: (...args: unknown[]) => void): void on(event: string, callback: (...args: unknown[]) => void): void
off(event: string, callback: (...args: unknown[]) => void): void off(event: string, callback: (...args: unknown[]) => void): void
getCenter(): { lng: number; lat: number } getCenter(): { lng: number; lat: number }
@@ -28,6 +30,8 @@ declare namespace AMap {
mapStyle?: string mapStyle?: string
features?: string[] features?: string[]
layers?: unknown[] layers?: unknown[]
skyColor?: string
maxPitch?: number
resizeEnable?: boolean resizeEnable?: boolean
dragEnable?: boolean dragEnable?: boolean
zoomEnable?: boolean zoomEnable?: boolean
+8 -54
View File
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { NAlert, NCard, NEmpty, NIcon, NProgress, NSkeleton } from 'naive-ui' import { NAlert, NCard, NEmpty, NIcon, NSkeleton } from 'naive-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useEncyclopediaStore } from '@/stores/encyclopedia' import { useEncyclopediaStore } from '@/stores/encyclopedia'
import EncyclopediaProgressCard from '@/components/cloud/EncyclopediaProgressCard.vue'
import { Lock } from '@vicons/tabler' import { Lock } from '@vicons/tabler'
import type { CloudType } from '@/types/database' import type { CloudType } from '@/types/database'
@@ -20,26 +21,6 @@ const rarityMeta = {
rare: { label: '罕见', chip: 'bg-rose-100 text-rose-700 border-rose-200', glow: 'from-rose-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 }> } satisfies Record<CloudType['rarity'], { label: string; chip: string; glow: string }>
function lerpHex(a: string, b: string, t: number) {
const ah = parseInt(a.slice(1), 16)
const bh = parseInt(b.slice(1), 16)
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 bv = Math.round((ah & 0xff) + ((bh & 0xff) - (ah & 0xff)) * t)
return `#${((r << 16) | (g << 8) | bv).toString(16).padStart(6, '0')}`
}
function progressColor(percent: number) {
const t = Math.min(1, Math.max(0, percent / 100))
if (t <= 0.5) return lerpHex('#0ea5e9', '#f59e0b', t * 2)
return lerpHex('#f59e0b', '#ef4444', (t - 0.5) * 2)
}
const totalTypes = computed(() => encyclopediaStore.cloudTypes.length || 10)
const unlockedCount = computed(() => (authStore.isLoggedIn ? encyclopediaStore.unlockedCount : 0))
const progressText = computed(() => `${unlockedCount.value}/${totalTypes.value}`)
const currentProgressColor = computed(() => progressColor(authStore.isLoggedIn ? encyclopediaStore.unlockPercent : 0))
function isUnlocked(cloudTypeId: number) { function isUnlocked(cloudTypeId: number) {
return authStore.isLoggedIn && encyclopediaStore.isUnlocked(cloudTypeId) return authStore.isLoggedIn && encyclopediaStore.isUnlocked(cloudTypeId)
} }
@@ -92,39 +73,12 @@ onMounted(async () => {
</p> </p>
</div> </div>
<NCard class="border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false"> <EncyclopediaProgressCard
<div class="flex items-center justify-between"> :unlocked-count="authStore.isLoggedIn ? encyclopediaStore.unlockedCount : 0"
<div> :total-count="encyclopediaStore.cloudTypes.length || 10"
<p class="text-sm text-slate-500">当前进度</p> :percent="encyclopediaStore.unlockPercent"
<p class="mt-1 text-3xl font-bold" :style="{ color: currentProgressColor }">{{ progressText }}</p> :is-logged-in="authStore.isLoggedIn"
</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: currentProgressColor, borderColor: currentProgressColor }"
>
{{ authStore.isLoggedIn ? encyclopediaStore.unlockPercent : 0 }}%
</div>
</div>
<NProgress
class="mt-5"
type="line"
:show-indicator="false"
:percentage="authStore.isLoggedIn ? encyclopediaStore.unlockPercent : 0"
:color="{ stops: ['#0ea5e9', currentProgressColor] }"
:height="12"
rail-color="#e2e8f0"
/> />
<p class="mt-4 text-sm text-slate-500">
<template v-if="authStore.isLoggedIn">
已解锁 {{ unlockedCount }} 枚徽章还差 {{ Math.max(totalTypes - unlockedCount, 0) }}
</template>
<template v-else>
登录后可同步你的个人图鉴进度
</template>
</p>
</NCard>
</div> </div>
</div> </div>
</div> </div>
+2
View File
@@ -409,6 +409,8 @@ onMounted(async () => {
mapStyle: 'amap://styles/normal', mapStyle: 'amap://styles/normal',
features: ['bg', 'road', 'building', 'point'], features: ['bg', 'road', 'building', 'point'],
resizeEnable: true, resizeEnable: true,
skyColor: '#c8dce8',
maxPitch: 70,
} as AMap.MapOptions) } as AMap.MapOptions)
mapInst.addControl(new AMapLib.Scale()) mapInst.addControl(new AMapLib.Scale())
+9 -23
View File
@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import { RouterLink, useRoute } from 'vue-router' import { RouterLink, useRoute } from 'vue-router'
import { NAlert, NButton, NCard, NDropdown, NEmpty, NIcon, NProgress, NSkeleton, NTag, useMessage } from 'naive-ui' import { NAlert, NButton, NCard, NDropdown, NEmpty, NIcon, NSkeleton, NTag, useMessage } from 'naive-ui'
import EncyclopediaProgressCard from '@/components/cloud/EncyclopediaProgressCard.vue'
import { Settings } from '@vicons/tabler' import { Settings } from '@vicons/tabler'
import CloudEditModal, { type CloudEditFormValue } from '@/components/cloud/CloudEditModal.vue' import CloudEditModal, { type CloudEditFormValue } from '@/components/cloud/CloudEditModal.vue'
import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue' import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue'
@@ -536,33 +537,18 @@ watch(selectedUploadDate, async newValue => {
</div> </div>
</div> </div>
<NCard class="border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false"> <EncyclopediaProgressCard
<template v-if="isOwnProfile"> v-if="isOwnProfile"
<p class="text-sm text-slate-500">图鉴进度</p> :unlocked-count="encyclopediaStore.unlockedCount"
<div class="mt-3 flex items-end justify-between gap-4"> :total-count="encyclopediaStore.cloudTypes.length || 10"
<div> :percent="encyclopediaStore.unlockPercent"
<p class="text-3xl font-bold text-slate-900">{{ encyclopediaStore.unlockProgress }}</p> :is-logged-in="true"
<p class="mt-2 text-sm text-slate-500">已解锁 {{ encyclopediaStore.unlockedCount }} 种基础云型</p>
</div>
<div class="border border-slate-900 bg-slate-900 px-4 py-3 text-xl font-semibold text-white">
{{ encyclopediaStore.unlockPercent }}%
</div>
</div>
<NProgress
class="mt-5"
type="line"
:show-indicator="false"
:percentage="encyclopediaStore.unlockPercent"
color="{stops:['#0ea5e9','#f59e0b']}"
:height="12"
/> />
</template>
<template v-else> <NCard v-else class="border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<p class="text-sm text-slate-500">公开贡献</p> <p class="text-sm text-slate-500">公开贡献</p>
<p class="mt-3 text-3xl font-bold text-slate-900">{{ approvedShots }}</p> <p class="mt-3 text-3xl font-bold text-slate-900">{{ approvedShots }}</p>
<p class="mt-2 text-sm text-slate-500">当前公开可见的云图数量</p> <p class="mt-2 text-sm text-slate-500">当前公开可见的云图数量</p>
</template>
</NCard> </NCard>
</div> </div>
</div> </div>