add leaderboard
Some checks failed
Deploy / deploy (build, debian, 22) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822) (push) Has been cancelled

This commit is contained in:
2026-03-09 20:00:11 +08:00
parent 9c577f9bc1
commit a7aa4f63ac
8 changed files with 173 additions and 3 deletions

1
components.d.ts vendored
View File

@@ -37,6 +37,7 @@ declare module 'vue' {
NPagination: typeof import('naive-ui')['NPagination'] NPagination: typeof import('naive-ui')['NPagination']
NPopover: typeof import('naive-ui')['NPopover'] NPopover: typeof import('naive-ui')['NPopover']
NRate: typeof import('naive-ui')['NRate'] NRate: typeof import('naive-ui')['NRate']
NSelect: typeof import('naive-ui')['NSelect']
NSpin: typeof import('naive-ui')['NSpin'] NSpin: typeof import('naive-ui')['NSpin']
NSplit: typeof import('naive-ui')['NSplit'] NSplit: typeof import('naive-ui')['NSplit']
NTab: typeof import('naive-ui')['NTab'] NTab: typeof import('naive-ui')['NTab']

View File

@@ -67,6 +67,11 @@ export const Account = {
const res = await http.post("/account/batch", payload) const res = await http.post("/account/batch", payload)
return res.data return res.data
}, },
async leaderboard() {
const res = await http.get("/account/leaderboard")
return res.data as { rank: number; username: string; total_score: number }[]
},
} }
export const Tutorial = { export const Tutorial = {
@@ -172,6 +177,18 @@ export const Submission = {
const res = await http.put(`/submission/${id}/flag`, { flag }) const res = await http.put(`/submission/${id}/flag`, { flag })
return res.data return res.data
}, },
async myScores() {
const res = await http.get("/submission/my-scores")
return res.data as {
task_id: number
task_display: number
task_title: string
score: number
created: string
}[]
},
} }
export const Prompt = { export const Prompt = {

View File

@@ -19,6 +19,14 @@
</template> </template>
<template #header-extra> <template #header-extra>
<n-tag type="warning" size="small">{{ item.score }}</n-tag> <n-tag type="warning" size="small">{{ item.score }}</n-tag>
<n-tag
v-if="myScoreMap.get(item.display)"
type="success"
size="small"
style="margin-left: 4px"
>
得分 {{ myScoreMap.get(item.display)!.toFixed(1) }}
</n-tag>
</template> </template>
</n-card> </n-card>
</n-gi> </n-gi>
@@ -31,7 +39,7 @@ import { ref, onMounted } from "vue"
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { marked } from "marked" import { marked } from "marked"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import { Challenge } from "../api" import { Challenge, Submission } from "../api"
import { taskTab, taskId, challengeDisplay } from "../store/task" import { taskTab, taskId, challengeDisplay } from "../store/task"
import { TASK_TYPE } from "../utils/const" import { TASK_TYPE } from "../utils/const"
import type { ChallengeSlim } from "../utils/type" import type { ChallengeSlim } from "../utils/type"
@@ -40,6 +48,16 @@ const router = useRouter()
const challenges = ref<ChallengeSlim[]>([]) const challenges = ref<ChallengeSlim[]>([])
const currentChallenge = ref<ChallengeSlim | null>(null) const currentChallenge = ref<ChallengeSlim | null>(null)
const content = ref("") const content = ref("")
const myScoreMap = ref<Map<number, number>>(new Map())
async function loadMyScores() {
try {
const scores = await Submission.myScores()
myScoreMap.value = new Map(scores.map((s) => [s.task_display, s.score]))
} catch {
// 未登录时忽略
}
}
async function loadList() { async function loadList() {
challenges.value = await Challenge.listDisplay() challenges.value = await Challenge.listDisplay()
@@ -72,7 +90,10 @@ function back() {
router.push({ name: "home-challenge-list" }) router.push({ name: "home-challenge-list" })
} }
onMounted(loadList) onMounted(async () => {
await loadList()
await loadMyScores()
})
</script> </script>
<style scoped> <style scoped>
.container { .container {

View File

@@ -44,6 +44,12 @@
> >
<Icon :width="16" icon="lucide:list"></Icon> <Icon :width="16" icon="lucide:list"></Icon>
</n-button> </n-button>
<n-button text @click="$router.push({ name: 'leaderboard' })">
<Icon :width="16" icon="lucide:trophy" />
</n-button>
<n-button text v-if="isLoggedIn" @click="$router.push({ name: 'my-scores' })">
<Icon :width="16" icon="lucide:bar-chart-2" />
</n-button>
<n-button text v-if="roleSuper" @click="edit"> <n-button text v-if="roleSuper" @click="edit">
<Icon :width="16" icon="lucide:edit"></Icon> <Icon :width="16" icon="lucide:edit"></Icon>
</n-button> </n-button>
@@ -63,7 +69,7 @@ import { step } from "../store/tutorial"
import { roleSuper } from "../store/user" import { roleSuper } from "../store/user"
import { taskTab, challengeDisplay } from "../store/task" import { taskTab, challengeDisplay } from "../store/task"
import { useRoute, useRouter } from "vue-router" import { useRoute, useRouter } from "vue-router"
import { TASK_TYPE } from "../utils/const" import { TASK_TYPE, STORAGE_KEY } from "../utils/const"
import Challenge from "./Challenge.vue" import Challenge from "./Challenge.vue"
import Tutorial from "./Tutorial.vue" import Tutorial from "./Tutorial.vue"
@@ -73,6 +79,8 @@ const tutorialRef = ref<InstanceType<typeof Tutorial>>()
defineEmits(["hide"]) defineEmits(["hide"])
const isLoggedIn = computed(() => localStorage.getItem(STORAGE_KEY.LOGIN) === "true")
const hideNav = computed( const hideNav = computed(
() => () =>
taskTab.value !== TASK_TYPE.Tutorial || taskTab.value !== TASK_TYPE.Tutorial ||

46
src/pages/Leaderboard.vue Normal file
View File

@@ -0,0 +1,46 @@
<template>
<div class="container">
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
<n-button secondary @click="$router.back()">返回</n-button>
<span style="font-weight: bold; font-size: 18px">排行榜</span>
<div style="width: 60px" />
</n-flex>
<n-data-table :columns="columns" :data="data" :loading="loading" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from "vue"
import { Account } from "../api"
import type { DataTableColumn } from "naive-ui"
const data = ref<{ rank: number; username: string; total_score: number }[]>([])
const loading = ref(false)
const columns: DataTableColumn<(typeof data.value)[0]>[] = [
{ title: "排名", key: "rank", width: 70 },
{ title: "用户名", key: "username" },
{
title: "总分",
key: "total_score",
render: (row) => h("span", { style: "font-weight: bold" }, row.total_score.toFixed(2)),
},
]
onMounted(async () => {
loading.value = true
try {
data.value = await Account.leaderboard()
} finally {
loading.value = false
}
})
</script>
<style scoped>
.container {
max-width: 600px;
margin: 40px auto;
padding: 0 16px;
}
</style>

64
src/pages/MyScores.vue Normal file
View File

@@ -0,0 +1,64 @@
<template>
<div class="container">
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
<n-button secondary @click="$router.back()">返回</n-button>
<span style="font-weight: bold; font-size: 18px">我的成绩</span>
<div style="width: 60px" />
</n-flex>
<n-empty v-if="!loading && !data.length" description="暂无评分记录" />
<n-data-table v-else :columns="columns" :data="data" :loading="loading" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from "vue"
import { Submission } from "../api"
import type { DataTableColumn } from "naive-ui"
import { parseTime } from "../utils/helper"
type MyScore = {
task_id: number
task_display: number
task_title: string
score: number
created: string
}
const data = ref<MyScore[]>([])
const loading = ref(false)
const columns: DataTableColumn<MyScore>[] = [
{ title: "题号", key: "task_display", width: 70 },
{ title: "标题", key: "task_title" },
{
title: "最高得分",
key: "score",
render: (row) =>
row.score > 0
? h("span", { style: { fontWeight: "bold", color: "#18a058" } }, row.score.toFixed(2))
: h("span", { style: { color: "#999" } }, "未评分"),
},
{
title: "提交时间",
key: "created",
render: (row) => parseTime(row.created, "YYYY-MM-DD HH:mm"),
},
]
onMounted(async () => {
loading.value = true
try {
data.value = await Submission.myScores()
} finally {
loading.value = false
}
})
</script>
<style scoped>
.container {
max-width: 700px;
margin: 40px auto;
padding: 0 16px;
}
</style>

View File

@@ -393,6 +393,8 @@ watch(
init() init()
}, },
) )
onMounted(init) onMounted(init)
onUnmounted(() => { onUnmounted(() => {
submission.value = { submission.value = {

View File

@@ -25,6 +25,17 @@ const routes = [
component: () => import("./pages/Submission.vue"), component: () => import("./pages/Submission.vue"),
props: true, props: true,
}, },
{
path: "/leaderboard",
name: "leaderboard",
component: () => import("./pages/Leaderboard.vue"),
},
{
path: "/my-scores",
name: "my-scores",
component: () => import("./pages/MyScores.vue"),
meta: { auth: true },
},
{ {
path: "/dashboard", path: "/dashboard",
name: "dashboard", name: "dashboard",