add leaderboard
This commit is contained in:
17
src/api.ts
17
src/api.ts
@@ -67,6 +67,11 @@ export const Account = {
|
||||
const res = await http.post("/account/batch", payload)
|
||||
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 = {
|
||||
@@ -172,6 +177,18 @@ export const Submission = {
|
||||
const res = await http.put(`/submission/${id}/flag`, { flag })
|
||||
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 = {
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<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>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
@@ -31,7 +39,7 @@ import { ref, onMounted } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { marked } from "marked"
|
||||
import { useRouter } from "vue-router"
|
||||
import { Challenge } from "../api"
|
||||
import { Challenge, Submission } from "../api"
|
||||
import { taskTab, taskId, challengeDisplay } from "../store/task"
|
||||
import { TASK_TYPE } from "../utils/const"
|
||||
import type { ChallengeSlim } from "../utils/type"
|
||||
@@ -40,6 +48,16 @@ const router = useRouter()
|
||||
const challenges = ref<ChallengeSlim[]>([])
|
||||
const currentChallenge = ref<ChallengeSlim | null>(null)
|
||||
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() {
|
||||
challenges.value = await Challenge.listDisplay()
|
||||
@@ -72,7 +90,10 @@ function back() {
|
||||
router.push({ name: "home-challenge-list" })
|
||||
}
|
||||
|
||||
onMounted(loadList)
|
||||
onMounted(async () => {
|
||||
await loadList()
|
||||
await loadMyScores()
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
>
|
||||
<Icon :width="16" icon="lucide:list"></Icon>
|
||||
</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">
|
||||
<Icon :width="16" icon="lucide:edit"></Icon>
|
||||
</n-button>
|
||||
@@ -63,7 +69,7 @@ import { step } from "../store/tutorial"
|
||||
import { roleSuper } from "../store/user"
|
||||
import { taskTab, challengeDisplay } from "../store/task"
|
||||
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 Tutorial from "./Tutorial.vue"
|
||||
|
||||
@@ -73,6 +79,8 @@ const tutorialRef = ref<InstanceType<typeof Tutorial>>()
|
||||
|
||||
defineEmits(["hide"])
|
||||
|
||||
const isLoggedIn = computed(() => localStorage.getItem(STORAGE_KEY.LOGIN) === "true")
|
||||
|
||||
const hideNav = computed(
|
||||
() =>
|
||||
taskTab.value !== TASK_TYPE.Tutorial ||
|
||||
|
||||
46
src/pages/Leaderboard.vue
Normal file
46
src/pages/Leaderboard.vue
Normal 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
64
src/pages/MyScores.vue
Normal 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>
|
||||
@@ -393,6 +393,8 @@ watch(
|
||||
init()
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
onMounted(init)
|
||||
onUnmounted(() => {
|
||||
submission.value = {
|
||||
|
||||
@@ -25,6 +25,17 @@ const routes = [
|
||||
component: () => import("./pages/Submission.vue"),
|
||||
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",
|
||||
name: "dashboard",
|
||||
|
||||
Reference in New Issue
Block a user