@@ -4,8 +4,9 @@ import { NH2, NH3 } from "naive-ui"
|
||||
import { getProfile } from "shared/api"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { durationToDays, parseTime } from "utils/functions"
|
||||
import { Profile } from "utils/types"
|
||||
import { getMetrics } from "../api"
|
||||
import { Profile, UserBadge as UserBadgeType } from "utils/types"
|
||||
import { getMetrics, getUserBadges } from "../api"
|
||||
import GroupedUserBadge from "shared/components/GroupedUserBadge.vue"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -15,11 +16,46 @@ const firstSubmissionAt = ref("")
|
||||
const latestSubmissionAt = ref("")
|
||||
const toLatestAt = ref("")
|
||||
const learnDuration = ref("")
|
||||
const userBadges = ref<GroupedBadge[]>([])
|
||||
const [loading, toggle] = useToggle()
|
||||
const [show, toggleShow] = useToggle(false)
|
||||
|
||||
const { isDesktop } = useBreakpoints()
|
||||
|
||||
// 分组徽章接口
|
||||
interface GroupedBadge {
|
||||
icon: string
|
||||
count: number
|
||||
badges: UserBadgeType[]
|
||||
latestEarnedTime: Date
|
||||
}
|
||||
|
||||
// 按图标分组徽章
|
||||
function groupBadgesByIcon(badges: UserBadgeType[]): GroupedBadge[] {
|
||||
const grouped = new Map<string, UserBadgeType[]>()
|
||||
|
||||
// 按图标分组
|
||||
badges.forEach((badge) => {
|
||||
const icon = badge.badge.icon
|
||||
if (!grouped.has(icon)) {
|
||||
grouped.set(icon, [])
|
||||
}
|
||||
grouped.get(icon)!.push(badge)
|
||||
})
|
||||
|
||||
// 转换为数组并排序
|
||||
return Array.from(grouped.entries())
|
||||
.map(([icon, badgeList]) => ({
|
||||
icon,
|
||||
count: badgeList.length,
|
||||
badges: badgeList,
|
||||
latestEarnedTime: new Date(
|
||||
Math.max(...badgeList.map((b) => new Date(b.earned_time).getTime())),
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.icon.localeCompare(b.icon))
|
||||
}
|
||||
|
||||
async function init() {
|
||||
toggle(true)
|
||||
try {
|
||||
@@ -50,6 +86,9 @@ async function init() {
|
||||
metricsRes.data.latest,
|
||||
)
|
||||
}
|
||||
// 获取用户徽章
|
||||
const badgesRes = await getUserBadges()
|
||||
userBadges.value = groupBadgesByIcon(badgesRes.data)
|
||||
} finally {
|
||||
toggle(false)
|
||||
}
|
||||
@@ -133,6 +172,24 @@ onMounted(init)
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<!-- 徽章展示卡片 -->
|
||||
<n-card
|
||||
v-if="!loading && profile && userBadges.length > 0"
|
||||
class="wrapper"
|
||||
hoverable
|
||||
>
|
||||
<template #header>
|
||||
<n-h4 style="margin: 0">获得的徽章</n-h4>
|
||||
</template>
|
||||
<n-flex wrap>
|
||||
<GroupedUserBadge
|
||||
v-for="groupedBadge in userBadges"
|
||||
:key="groupedBadge.icon"
|
||||
:grouped-badge="groupedBadge"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<n-descriptions v-if="!loading && profile" class="wrapper" bordered>
|
||||
<n-descriptions-item v-if="!!problems.length">
|
||||
<template #label>
|
||||
|
||||
76
src/shared/components/GroupedUserBadge.vue
Normal file
76
src/shared/components/GroupedUserBadge.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="badge-item">
|
||||
<img
|
||||
:src="groupedBadge.icon"
|
||||
:alt="groupedBadge.badges[0].badge.name"
|
||||
class="badge-icon"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div v-if="groupedBadge.count > 1" class="badge-count">
|
||||
×{{ groupedBadge.count }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface GroupedBadge {
|
||||
icon: string
|
||||
count: number
|
||||
badges: any[]
|
||||
latestEarnedTime: Date
|
||||
}
|
||||
|
||||
interface Props {
|
||||
groupedBadge: GroupedBadge
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
function handleImageError(event: Event) {
|
||||
const img = event.target as HTMLImageElement
|
||||
img.src = "/badge-1.png"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.badge-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid #e0e0e0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.badge-icon:hover {
|
||||
transform: scale(1.1);
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.badge-count {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user