From d31bfd8b8f3c8cd2e76d11bacb324234d26e6c4f Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 4 Aug 2026 06:57:07 -0600 Subject: [PATCH] =?UTF-8?q?feat(achievement):=20=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=B8=BB=E9=A1=B5=E4=B8=8E=E6=8E=92=E8=A1=8C=E6=A6=9C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=88=90=E5=B0=B1=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 个人主页显示完成度与最近获得的成就,可跳转奖杯馆; 全服 Top100 和班级排名每行加一个跳转按钮。 成就摘要单独请求,不并进 init 里那个按位置取值的 promises 数组, 且失败时静默降级——它不该拖垮整个个人主页。 Co-Authored-By: Claude Opus 5 --- src/oj/rank/list.vue | 95 +++++++++++++++++++++++++++---------------- src/oj/user/index.vue | 78 ++++++++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 37 deletions(-) diff --git a/src/oj/rank/list.vue b/src/oj/rank/list.vue index d911614..0b94a7e 100644 --- a/src/oj/rank/list.vue +++ b/src/oj/rank/list.vue @@ -198,17 +198,30 @@ const columns: DataTableColumn[] = [ "streamline-emojis:smiling-face-with-sunglasses", ), key: "username", - width: 200, + width: 240, render: (row) => - h( - NButton, - { - text: true, - type: "info", - onClick: () => router.push("/user?name=" + row.user.username), - }, - () => row.user.username, - ), + h("div", { style: "display:flex;align-items:center;gap:6px" }, [ + h( + NButton, + { + text: true, + type: "info", + onClick: () => router.push("/user?name=" + row.user.username), + }, + () => row.user.username, + ), + h( + NButton, + { + text: true, + size: "tiny", + title: "查看成就", + onClick: () => + router.push("/achievement?name=" + row.user.username), + }, + () => "🏆", + ), + ]), }, { title: renderTableTitle( @@ -378,32 +391,44 @@ const myClassColumns: DataTableColumn[] = [ { title: "用户名", key: "username", - width: 200, + width: 240, render: (row) => - h( - NButton, - { - text: true, - type: "info", - onClick: () => router.push("/user?name=" + row.username), - }, - () => - row.rank === myRank.value - ? h( - NFlex, - { align: "flex-end" }, - { - default: () => [ - h("span", {}, row.username), - h(Icon, { - width: 20, - icon: "fluent-emoji:person-raising-hand", - }), - ], - }, - ) - : row.username, - ), + h("div", { style: "display:flex;align-items:center;gap:6px" }, [ + h( + NButton, + { + text: true, + type: "info", + onClick: () => router.push("/user?name=" + row.username), + }, + () => + row.rank === myRank.value + ? h( + NFlex, + { align: "flex-end" }, + { + default: () => [ + h("span", {}, row.username), + h(Icon, { + width: 20, + icon: "fluent-emoji:person-raising-hand", + }), + ], + }, + ) + : row.username, + ), + h( + NButton, + { + text: true, + size: "tiny", + title: "查看成就", + onClick: () => router.push("/achievement?name=" + row.username), + }, + () => "🏆", + ), + ]), }, { title: "已解决", diff --git a/src/oj/user/index.vue b/src/oj/user/index.vue index 55ba92d..b9c2588 100644 --- a/src/oj/user/index.vue +++ b/src/oj/user/index.vue @@ -4,7 +4,12 @@ import { NH2, NH3 } from "naive-ui" import { getProfile } from "shared/api" import { useBreakpoints } from "shared/composables/breakpoints" 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 GroupedUserBadge from "shared/components/GroupedUserBadge.vue" import { useUserStore } from "shared/store/user" @@ -19,6 +24,7 @@ const latestSubmissionAt = ref("") const toLatestAt = ref("") const learnDuration = ref("") const userBadges = ref([]) +const achievementSummary = ref(null) const [loading, toggle] = useToggle() 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(() => { if (loading.value) return [] return [ @@ -180,7 +199,10 @@ const metrics = computed(() => { ] }) -onMounted(init) +onMounted(() => { + init() + loadAchievementSummary() +})