feat(achievement): 个人主页与排行榜添加成就入口
个人主页显示完成度与最近获得的成就,可跳转奖杯馆; 全服 Top100 和班级排名每行加一个跳转按钮。 成就摘要单独请求,不并进 init 里那个按位置取值的 promises 数组, 且失败时静默降级——它不该拖垮整个个人主页。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -198,17 +198,30 @@ const columns: DataTableColumn<Rank>[] = [
|
|||||||
"streamline-emojis:smiling-face-with-sunglasses",
|
"streamline-emojis:smiling-face-with-sunglasses",
|
||||||
),
|
),
|
||||||
key: "username",
|
key: "username",
|
||||||
width: 200,
|
width: 240,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(
|
h("div", { style: "display:flex;align-items:center;gap:6px" }, [
|
||||||
NButton,
|
h(
|
||||||
{
|
NButton,
|
||||||
text: true,
|
{
|
||||||
type: "info",
|
text: true,
|
||||||
onClick: () => router.push("/user?name=" + row.user.username),
|
type: "info",
|
||||||
},
|
onClick: () => router.push("/user?name=" + row.user.username),
|
||||||
() => row.user.username,
|
},
|
||||||
),
|
() => row.user.username,
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
size: "tiny",
|
||||||
|
title: "查看成就",
|
||||||
|
onClick: () =>
|
||||||
|
router.push("/achievement?name=" + row.user.username),
|
||||||
|
},
|
||||||
|
() => "🏆",
|
||||||
|
),
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle(
|
title: renderTableTitle(
|
||||||
@@ -378,32 +391,44 @@ const myClassColumns: DataTableColumn<UserRank>[] = [
|
|||||||
{
|
{
|
||||||
title: "用户名",
|
title: "用户名",
|
||||||
key: "username",
|
key: "username",
|
||||||
width: 200,
|
width: 240,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(
|
h("div", { style: "display:flex;align-items:center;gap:6px" }, [
|
||||||
NButton,
|
h(
|
||||||
{
|
NButton,
|
||||||
text: true,
|
{
|
||||||
type: "info",
|
text: true,
|
||||||
onClick: () => router.push("/user?name=" + row.username),
|
type: "info",
|
||||||
},
|
onClick: () => router.push("/user?name=" + row.username),
|
||||||
() =>
|
},
|
||||||
row.rank === myRank.value
|
() =>
|
||||||
? h(
|
row.rank === myRank.value
|
||||||
NFlex,
|
? h(
|
||||||
{ align: "flex-end" },
|
NFlex,
|
||||||
{
|
{ align: "flex-end" },
|
||||||
default: () => [
|
{
|
||||||
h("span", {}, row.username),
|
default: () => [
|
||||||
h(Icon, {
|
h("span", {}, row.username),
|
||||||
width: 20,
|
h(Icon, {
|
||||||
icon: "fluent-emoji:person-raising-hand",
|
width: 20,
|
||||||
}),
|
icon: "fluent-emoji:person-raising-hand",
|
||||||
],
|
}),
|
||||||
},
|
],
|
||||||
)
|
},
|
||||||
: row.username,
|
)
|
||||||
),
|
: row.username,
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
size: "tiny",
|
||||||
|
title: "查看成就",
|
||||||
|
onClick: () => router.push("/achievement?name=" + row.username),
|
||||||
|
},
|
||||||
|
() => "🏆",
|
||||||
|
),
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "已解决",
|
title: "已解决",
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { NH2, NH3 } from "naive-ui"
|
|||||||
import { getProfile } from "shared/api"
|
import { getProfile } from "shared/api"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { durationToDays, parseTime } from "utils/functions"
|
import { durationToDays, parseTime } from "utils/functions"
|
||||||
import type { Profile, UserBadge as UserBadgeType } from "utils/types"
|
import type {
|
||||||
|
AchievementSummary,
|
||||||
|
Profile,
|
||||||
|
UserBadge as UserBadgeType,
|
||||||
|
} from "utils/types"
|
||||||
|
import { getAchievementSummary } from "oj/achievement/api"
|
||||||
import { getMetrics, getUserBadges } from "../api"
|
import { getMetrics, getUserBadges } from "../api"
|
||||||
import GroupedUserBadge from "shared/components/GroupedUserBadge.vue"
|
import GroupedUserBadge from "shared/components/GroupedUserBadge.vue"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
@@ -19,6 +24,7 @@ const latestSubmissionAt = ref("")
|
|||||||
const toLatestAt = ref("")
|
const toLatestAt = ref("")
|
||||||
const learnDuration = ref("")
|
const learnDuration = ref("")
|
||||||
const userBadges = ref<GroupedBadge[]>([])
|
const userBadges = ref<GroupedBadge[]>([])
|
||||||
|
const achievementSummary = ref<AchievementSummary | null>(null)
|
||||||
const [loading, toggle] = useToggle()
|
const [loading, toggle] = useToggle()
|
||||||
const [show, toggleShow] = useToggle(false)
|
const [show, toggleShow] = useToggle(false)
|
||||||
|
|
||||||
@@ -142,6 +148,19 @@ async function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 单独取,不塞进上面的 promises 数组:那里是按位置取 results[0]/[1] 的,
|
||||||
|
// 插一项进去会打乱既有索引。成就摘要取不到也不该影响整个个人主页
|
||||||
|
async function loadAchievementSummary() {
|
||||||
|
try {
|
||||||
|
const res = await getAchievementSummary(
|
||||||
|
(route.query.name as string) || undefined,
|
||||||
|
)
|
||||||
|
achievementSummary.value = res.data
|
||||||
|
} catch {
|
||||||
|
achievementSummary.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const metrics = computed(() => {
|
const metrics = computed(() => {
|
||||||
if (loading.value) return []
|
if (loading.value) return []
|
||||||
return [
|
return [
|
||||||
@@ -180,7 +199,10 @@ const metrics = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(init)
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
loadAchievementSummary()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<n-flex
|
<n-flex
|
||||||
@@ -244,6 +266,48 @@ onMounted(init)
|
|||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|
||||||
|
<!-- 成就摘要 -->
|
||||||
|
<n-card
|
||||||
|
v-if="!loading && profile && achievementSummary"
|
||||||
|
class="wrapper"
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<n-flex align="center" justify="space-between">
|
||||||
|
<n-flex align="center" :size="12">
|
||||||
|
<span class="achievement-title">
|
||||||
|
成就 {{ achievementSummary.unlocked }} /
|
||||||
|
{{ achievementSummary.total }}
|
||||||
|
</span>
|
||||||
|
<n-tag size="small" type="info">
|
||||||
|
{{ achievementSummary.percent }}%
|
||||||
|
</n-tag>
|
||||||
|
</n-flex>
|
||||||
|
<n-button
|
||||||
|
text
|
||||||
|
type="primary"
|
||||||
|
@click="
|
||||||
|
router.push({
|
||||||
|
path: '/achievement',
|
||||||
|
query: route.query.name ? { name: route.query.name } : {},
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
查看全部
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex align="center" :size="10" class="achievement-recent">
|
||||||
|
<n-tooltip v-for="a in achievementSummary.recent" :key="a.id">
|
||||||
|
<template #trigger>
|
||||||
|
<span class="achievement-icon">{{ a.icon }}</span>
|
||||||
|
</template>
|
||||||
|
{{ a.name }}
|
||||||
|
</n-tooltip>
|
||||||
|
<n-text v-if="!achievementSummary.recent.length" depth="3">
|
||||||
|
还没有获得成就
|
||||||
|
</n-text>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
<!-- 徽章展示卡片 -->
|
<!-- 徽章展示卡片 -->
|
||||||
<n-card
|
<n-card
|
||||||
v-if="!loading && profile && userBadges.length > 0"
|
v-if="!loading && profile && userBadges.length > 0"
|
||||||
@@ -321,4 +385,14 @@ h2 {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.achievement-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.achievement-recent {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.achievement-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user