重构排名
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2026-01-04 15:53:35 +08:00
parent 2becfedafd
commit e073ef240c
6 changed files with 339 additions and 338 deletions

View File

@@ -143,18 +143,18 @@ export function getActivityRank(start: string) {
}) })
} }
export function getClassRank( export function getClassRank(grade?: number | null) {
offset: number,
limit: number,
grade?: number | null,
) {
return http.get("class_rank", { return http.get("class_rank", {
params: { offset, limit, grade }, params: { grade },
}) })
} }
export function getUserClassRank() { export function getUserClassRank(
return http.get("user_class_rank") scope?: "all" | "window",
offset?: number,
limit?: number,
) {
return http.get("user_class_rank", { params: { scope, offset, limit } })
} }
export function getClassPK( export function getClassPK(

View File

@@ -1,95 +0,0 @@
<script setup lang="ts">
import { getUserClassRank } from "oj/api"
import { useUserStore } from "shared/store/user"
import { NButton } from "naive-ui"
const userStore = useUserStore()
const router = useRouter()
const message = useMessage()
interface UserRank {
rank: number
username: string
accepted_number: number
submission_number: number
}
const myRank = ref(-1)
const className = ref("")
const data = ref<UserRank[]>([])
const loading = ref(false)
const columns: DataTableColumn<UserRank>[] = [
{
title: "排名",
key: "rank",
width: 100,
align: "center",
},
{
title: "用户名",
key: "username",
width: 200,
render: (row) =>
h(
NButton,
{
text: true,
type: "info",
onClick: () => router.push("/user?name=" + row.username),
},
() => row.username,
),
},
{
title: "AC数",
key: "accepted_number",
width: 120,
align: "center",
},
{
title: "提交数",
key: "submission_number",
width: 120,
align: "center",
},
]
async function init() {
loading.value = true
try {
if (!userStore.user) {
await userStore.getMyProfile()
}
const user = userStore.user
if (!user || !user.class_name) {
message.warning("您没有班级信息")
return
}
const res = await getUserClassRank()
myRank.value = res.data.my_rank
className.value = res.data.class_name
data.value = res.data.ranks
} finally {
loading.value = false
}
}
onMounted(init)
</script>
<template>
<n-flex vertical size="large" v-if="!loading">
<n-h2>我的班级排名</n-h2>
<n-alert v-if="className" type="info">
班级{{ className }} | 我的排名{{
myRank > 0 ? `${myRank}` : "暂无排名"
}}
| 班级总人数{{ data.length }}
</n-alert>
<n-alert v-else type="warning"> 您还没有加入班级 </n-alert>
<n-data-table :data="data" :columns="columns" />
</n-flex>
</template>

View File

@@ -1,144 +0,0 @@
<script setup lang="ts">
import { getClassRank } from "oj/api"
import Pagination from "shared/components/Pagination.vue"
interface ClassRank {
rank: number
class_name: string
user_count: number
total_ac: number
total_submission: number
avg_ac: number
ac_rate: number
}
const data = ref<ClassRank[]>([])
const total = ref(0)
const query = reactive({
limit: 10,
page: 1,
grade: null as number | null,
})
const gradeOptions = [
{ label: "24年级", value: 24 },
{ label: "23年级", value: 23 },
{ label: "22年级", value: 22 },
{ label: "21年级", value: 21 },
{ label: "20年级", value: 20 },
]
const columns: DataTableColumn<ClassRank>[] = [
{
title: "排名",
key: "rank",
width: 100,
titleAlign: "center",
align: "center",
render: (row) => {
if (row.rank === 1) return "🥇"
if (row.rank === 2) return "🥈"
if (row.rank === 3) return "🥉"
return row.rank
},
},
{
title: "班级",
key: "class_name",
width: 200,
titleAlign: "center",
align: "center",
},
{
title: "人数",
key: "user_count",
width: 100,
titleAlign: "center",
align: "center",
},
{
title: "总AC数",
key: "total_ac",
width: 120,
titleAlign: "center",
align: "center",
},
{
title: "总提交数",
key: "total_submission",
width: 120,
titleAlign: "center",
align: "center",
},
{
title: "平均AC数",
key: "avg_ac",
width: 120,
titleAlign: "center",
align: "center",
},
{
title: "正确率",
key: "ac_rate",
width: 100,
titleAlign: "center",
align: "center",
render: (row) => `${row.ac_rate}%`,
},
]
async function init() {
if (query.grade === null) {
data.value = []
total.value = 0
return
}
const offset = (query.page - 1) * query.limit
const res = await getClassRank(offset, query.limit, query.grade)
data.value = res.data.results
total.value = res.data.total
}
watch(() => query.page, init)
watch(
() => query.limit,
() => {
query.page = 1
init()
},
)
watch(
() => query.grade,
() => {
query.page = 1
init()
},
)
onMounted(() => {
if (query.grade !== null) {
init()
}
})
</script>
<template>
<n-flex justify="center">
<n-h2>班级排名</n-h2>
</n-flex>
<n-flex justify="center" style="margin-bottom: 16px">
<n-select
v-model:value="query.grade"
placeholder="选择年级"
clearable
style="width: 200px"
:options="gradeOptions"
/>
</n-flex>
<n-data-table :data="data" :columns="columns" />
<Pagination
:total="total"
v-model:page="query.page"
v-model:limit="query.limit"
/>
</template>

View File

@@ -1,7 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { formatISO, sub, type Duration } from "date-fns" import { formatISO, sub, type Duration } from "date-fns"
import { NButton } from "naive-ui" import { NButton, NFlex, useThemeVars } from "naive-ui"
import { getActivityRank, getRank } from "oj/api" import {
getActivityRank,
getClassRank,
getRank,
getUserClassRank,
} from "oj/api"
import { useBreakpoints } from "shared/composables/breakpoints"
import { getACRate } from "utils/functions" import { getACRate } from "utils/functions"
import { Rank } from "utils/types" import { Rank } from "utils/types"
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
@@ -9,17 +15,60 @@ import { ChartType } from "utils/constants"
import { renderTableTitle } from "utils/renders" import { renderTableTitle } from "utils/renders"
import Chart from "./components/Chart.vue" import Chart from "./components/Chart.vue"
import Index from "./components/Index.vue" import Index from "./components/Index.vue"
import { useUserStore } from "shared/store/user"
import { Icon } from "@iconify/vue"
const gradeOptions = [
{ label: "24年级", value: 24 },
{ label: "23年级", value: 23 },
{ label: "22年级", value: 22 },
{ label: "21年级", value: 21 },
{ label: "20年级", value: 20 },
]
const router = useRouter() const router = useRouter()
const themeVars = useThemeVars()
const userStore = useUserStore()
const { isDesktop } = useBreakpoints()
const data = ref<Rank[]>([]) const data = ref<Rank[]>([])
const total = ref(0) const total = ref(0)
const query = reactive({ const query = reactive({
limit: 10, limit: 10,
page: 1, page: 1,
}) })
const chart = ref<Rank[]>([]) const rankChart = ref<Rank[]>([])
const chartType = ref(ChartType.Rank) const activityChart = ref<Rank[]>([])
const duration = ref("weeks:1") const duration = ref("months:1")
const classData = ref<ClassRank[]>([])
const classQuery = reactive({
grade: gradeOptions[0].value,
})
const myClassData = ref<UserRank[]>([])
const myRank = ref(-1)
const myClassName = ref("")
const myClassScope = ref<"window" | "all">("window")
const myClassTotal = ref(0)
const myClassQuery = reactive({
page: 1,
limit: 10,
})
interface ClassRank {
rank: number
class_name: string
user_count: number
total_ac: number
total_submission: number
avg_ac: number
ac_rate: number
}
interface UserRank {
rank: number
username: string
accepted_number: number
submission_number: number
}
async function init() { async function init() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
@@ -96,24 +145,23 @@ watch(
watch(duration, listActivity) watch(duration, listActivity)
async function listActivity() { async function listActivity() {
chartType.value = ChartType.Activity
const current = Date.now() const current = Date.now()
const start = formatISO(sub(current, subOptions.value)) const start = formatISO(sub(current, subOptions.value))
const res = await getActivityRank(start) const res = await getActivityRank(start)
chart.value = res.data.map((d: { username: string; count: number }) => ({ activityChart.value = res.data.map(
(d: { username: string; count: number }) => ({
user: { user: {
username: d.username, username: d.username,
}, },
accepted_number: d.count, accepted_number: d.count,
submission_number: 0, submission_number: 0,
})) }),
)
} }
async function listRank() { async function listRank() {
chartType.value = ChartType.Rank
const res = await getRank(0, 10, 10) const res = await getRank(0, 10, 10)
data.value = res.data.results rankChart.value = res.data.results
chart.value = data.value
} }
const options: SelectOption[] = [ const options: SelectOption[] = [
@@ -125,51 +173,279 @@ const options: SelectOption[] = [
] ]
const subOptions = computed<Duration>(() => { const subOptions = computed<Duration>(() => {
let dur = options.find((it) => it.value === duration.value) ?? options[0] let dur = options.find((it) => it.value === duration.value) ?? options[1]
const x = dur.value!.toString().split(":") const x = dur.value!.toString().split(":")
const unit = x[0] const unit = x[0]
const n = x[1] const n = x[1]
return { [unit]: parseInt(n) } return { [unit]: parseInt(n) }
}) })
onMounted(async () => { onMounted(() => {
chart.value = await init() init()
listRank()
listActivity()
listClassRank()
listMyClassRank()
}) })
const classColumns: DataTableColumn<ClassRank>[] = [
{
title: "排名",
key: "rank",
width: 100,
titleAlign: "center",
align: "center",
render: (row) => {
if (row.rank === 1) return "🥇"
if (row.rank === 2) return "🥈"
if (row.rank === 3) return "🥉"
return row.rank
},
},
{
title: "班级",
key: "class_name",
width: 200,
titleAlign: "center",
align: "center",
},
{
title: "人数",
key: "user_count",
width: 100,
titleAlign: "center",
align: "center",
},
{
title: "总AC数",
key: "total_ac",
width: 120,
titleAlign: "center",
align: "center",
},
{
title: "总提交数",
key: "total_submission",
width: 120,
titleAlign: "center",
align: "center",
},
{
title: "平均AC数",
key: "avg_ac",
width: 120,
titleAlign: "center",
align: "center",
},
{
title: "正确率",
key: "ac_rate",
width: 100,
titleAlign: "center",
align: "center",
render: (row) => `${row.ac_rate}%`,
},
]
const myClassColumns: DataTableColumn<UserRank>[] = [
{
title: "排名",
key: "rank",
width: 100,
align: "center",
},
{
title: "用户名",
key: "username",
width: 200,
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,
),
},
{
title: "已解决",
key: "accepted_number",
width: 120,
align: "center",
},
{
title: "提交数",
key: "submission_number",
width: 120,
align: "center",
},
]
async function listClassRank() {
if (!userStore.user) {
await userStore.getMyProfile()
}
const className = userStore.user?.class_name
if (className) {
classQuery.grade = parseInt(className.slice(0, 2))
}
const res = await getClassRank(classQuery.grade)
classData.value = res.data
}
async function listMyClassRank() {
try {
const offset =
myClassScope.value === "all"
? (myClassQuery.page - 1) * myClassQuery.limit
: 0
const limit = myClassScope.value === "all" ? myClassQuery.limit : undefined
const res = await getUserClassRank(myClassScope.value, offset, limit)
myRank.value = res.data.my_rank
myClassName.value = res.data.class_name
myClassData.value = res.data.ranks
myClassTotal.value = res.data.total ?? res.data.ranks.length
if (myClassScope.value === "window") {
myClassQuery.page = 1
}
} catch (err: any) {
console.error(err)
}
}
watch(
() => classQuery.grade,
() => {
listClassRank()
},
)
watch(myClassScope, listMyClassRank)
watch(
() => myClassQuery.page,
() => {
if (myClassScope.value === "all") {
listMyClassRank()
}
},
)
watch(
() => myClassQuery.limit,
() => {
myClassQuery.page = 1
if (myClassScope.value === "all") {
listMyClassRank()
}
},
)
</script> </script>
<template> <template>
<n-flex justify="center"> <n-flex vertical size="large">
<n-button-group> <n-grid :cols="isDesktop ? 2 : 1" :x-gap="20" :y-gap="20">
<n-button <n-gi :span="1">
@click="listRank" <n-card>
:type="chartType === ChartType.Rank ? 'primary' : 'default'" <template #header>
> <div style="height: 34px">全服 Top10</div>
天梯排名 </template>
</n-button> <Chart
<n-button v-if="rankChart.length"
@click="listActivity" :type="ChartType.Rank"
:type="chartType === ChartType.Activity ? 'primary' : 'default'" :rank-data="rankChart"
> />
活跃度排名 <n-empty v-else style="padding: 20px 0"></n-empty>
</n-button> </n-card>
</n-button-group> </n-gi>
<div v-if="chartType === ChartType.Activity"> <n-gi :span="1">
<n-card>
<template #header>日活 Top10</template>
<template #header-extra>
<n-select <n-select
style="width: 120px" style="width: 120px"
:options="options" :options="options"
v-model:value="duration" v-model:value="duration"
/> />
</div> </template>
</n-flex> <Chart
<Chart v-if="!!chart.length" :type="chartType" :rank-data="chart" /> v-if="activityChart.length"
:type="ChartType.Activity"
:rank-data="activityChart"
/>
<n-empty v-else style="padding: 20px 0"></n-empty> <n-empty v-else style="padding: 20px 0"></n-empty>
<n-flex justify="center"> </n-card>
<n-h2>全校前100名</n-h2> </n-gi>
</n-flex> </n-grid>
<n-card>
<template #header>全服 Top100</template>
<n-data-table :data="data" :columns="columns" /> <n-data-table :data="data" :columns="columns" />
<template #footer>
<Pagination <Pagination
:total="total" :total="total"
v-model:page="query.page" v-model:page="query.page"
v-model:limit="query.limit" v-model:limit="query.limit"
/> />
</template>
</n-card>
<n-grid :cols="isDesktop ? 2 : 1" :x-gap="20" :y-gap="20">
<n-gi :span="1">
<n-card>
<template #header>班级排名</template>
<template #header-extra>
<n-select
v-model:value="classQuery.grade"
placeholder="选择年级"
clearable
style="width: 180px"
:options="gradeOptions"
/>
</template>
<n-data-table :data="classData" :columns="classColumns" />
</n-card>
</n-gi>
<n-gi :span="1">
<n-card>
<template #header>我在班级的排名</template>
<template #header-extra>
<n-select
style="width: 180px"
:options="[
{ label: '我的位置', value: 'window' },
{ label: '全班排名', value: 'all' },
]"
v-model:value="myClassScope"
/>
</template>
<n-data-table :data="myClassData" :columns="myClassColumns" />
<template #footer v-if="myClassScope === 'all'">
<Pagination
:total="myClassTotal"
v-model:page="myClassQuery.page"
v-model:limit="myClassQuery.limit"
/>
</template>
</n-card>
</n-gi>
</n-grid>
</n-flex>
</template> </template>
<style scoped></style>

View File

@@ -68,22 +68,8 @@ export const ojs: RouteRecordRaw = {
}, },
{ {
path: "class", path: "class",
children: [
{
path: "rank",
component: () => import("oj/class/rank.vue"),
},
{
path: "pk",
component: () => import("oj/class/pk.vue"), component: () => import("oj/class/pk.vue"),
}, },
{
path: "my-rank",
component: () => import("oj/class/my-rank.vue"),
meta: { requiresAuth: true },
},
],
},
{ {
path: "announcement", path: "announcement",
component: () => import("oj/announcement/list.vue"), component: () => import("oj/announcement/list.vue"),

View File

@@ -119,32 +119,10 @@ const menus = computed<MenuOption[]>(() => [
icon: renderIcon("streamline-emojis:hibiscus"), icon: renderIcon("streamline-emojis:hibiscus"),
}, },
{ {
label: () => "班级", label: () => h(RouterLink, { to: "/class/pk" }, { default: () => "班级" }),
key: "class",
show: false, show: false,
key: "class",
icon: renderIcon("twemoji:crossed-swords"), icon: renderIcon("twemoji:crossed-swords"),
children: [
{
label: () =>
h(RouterLink, { to: "/class/rank" }, { default: () => "班级排名" }),
key: "class-rank",
},
{
label: () =>
h(RouterLink, { to: "/class/pk" }, { default: () => "班级PK" }),
key: "class-pk",
},
{
label: () =>
h(
RouterLink,
{ to: "/class/my-rank" },
{ default: () => "我的排名" },
),
key: "my-rank",
show: userStore.isAuthed,
},
],
}, },
{ {
label: () => label: () =>