diff --git a/src/admin/achievement/components/AchievementModal.vue b/src/admin/achievement/components/AchievementModal.vue new file mode 100644 index 0000000..bab15b1 --- /dev/null +++ b/src/admin/achievement/components/AchievementModal.vue @@ -0,0 +1,160 @@ + + + diff --git a/src/admin/achievement/list.vue b/src/admin/achievement/list.vue new file mode 100644 index 0000000..6c1f2f7 --- /dev/null +++ b/src/admin/achievement/list.vue @@ -0,0 +1,135 @@ + + + diff --git a/src/admin/api.ts b/src/admin/api.ts index 2d8f7c8..fe34e5b 100644 --- a/src/admin/api.ts +++ b/src/admin/api.ts @@ -531,3 +531,48 @@ export function pinAIReport(id: number) { export function getPinnedAIReports() { return http.get("admin/ai/reports", { params: { pinned_only: "true" } }) } + +// ==================== 成就 ==================== + +export interface AdminAchievement { + id: number + name: string + description: string + icon: string + rarity: string + hidden: boolean + metric: string + metric_name: string + operator: "gte" | "lte" + threshold: number + visible: boolean + unlock_count: number + order: number + create_time: string +} + +export interface MetricOption { + key: string + name: string + help_text: string +} + +export function getAdminAchievements() { + return http.get("admin/achievement") +} + +export function getMetricOptions() { + return http.get("admin/achievement/metrics") +} + +export function createAchievement(data: Partial) { + return http.post("admin/achievement", data) +} + +export function updateAchievement(data: Partial) { + return http.put("admin/achievement", data) +} + +export function deleteAchievement(id: number) { + return http.delete("admin/achievement", { params: { id } }) +} 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() +})