This commit is contained in:
@@ -8,4 +8,4 @@ body {
|
||||
|
||||
.md-editor-dark div.vuepress-theme {
|
||||
--md-theme-color: var(--n-text-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,11 @@ export function getActivityRank(start: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getClassRank(offset: number, limit: number, grade?: number | null) {
|
||||
export function getClassRank(
|
||||
offset: number,
|
||||
limit: number,
|
||||
grade?: number | null,
|
||||
) {
|
||||
return http.get("class_rank", {
|
||||
params: { offset, limit, grade },
|
||||
})
|
||||
@@ -155,7 +159,11 @@ export function getUserClassRank(offset: number, limit: number) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getClassPK(classNames: string[], startTime?: string, endTime?: string) {
|
||||
export function getClassPK(
|
||||
classNames: string[],
|
||||
startTime?: string,
|
||||
endTime?: string,
|
||||
) {
|
||||
const payload: any = {
|
||||
class_name: classNames,
|
||||
}
|
||||
|
||||
@@ -1,121 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { getUserClassRank } from "oj/api"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { renderTableTitle } from "utils/renders"
|
||||
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 totalUsers = ref(0)
|
||||
const className = ref("")
|
||||
const data = ref<UserRank[]>([])
|
||||
const total = ref(0)
|
||||
const query = reactive({
|
||||
limit: 20,
|
||||
page: 1,
|
||||
})
|
||||
|
||||
const columns: DataTableColumn<UserRank>[] = [
|
||||
{
|
||||
title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"),
|
||||
key: "rank",
|
||||
width: 100,
|
||||
align: "center",
|
||||
render: (row) => {
|
||||
if (row.rank === 1) return "🥇"
|
||||
if (row.rank === 2) return "🥈"
|
||||
if (row.rank === 3) return "🥉"
|
||||
return row.rank
|
||||
},
|
||||
},
|
||||
{
|
||||
title: renderTableTitle(
|
||||
"用户名",
|
||||
"streamline-emojis:smiling-face-with-sunglasses",
|
||||
),
|
||||
key: "username",
|
||||
width: 200,
|
||||
render: (row) =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: "info",
|
||||
onClick: () => router.push("/user?name=" + row.username),
|
||||
},
|
||||
() => row.username,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: renderTableTitle("AC数", "streamline-emojis:raised-fist-1"),
|
||||
key: "accepted_number",
|
||||
width: 120,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: renderTableTitle("提交数", "streamline-emojis:rocket"),
|
||||
key: "submission_number",
|
||||
width: 120,
|
||||
align: "center",
|
||||
},
|
||||
]
|
||||
|
||||
async function init() {
|
||||
const user = userStore.user
|
||||
if (!user || !user.class_name) {
|
||||
message.warning("您没有班级信息")
|
||||
return
|
||||
}
|
||||
|
||||
const offset = (query.page - 1) * query.limit
|
||||
const res = await getUserClassRank(offset, query.limit)
|
||||
myRank.value = res.data.my_rank
|
||||
totalUsers.value = res.data.total_users
|
||||
className.value = res.data.class_name
|
||||
data.value = res.data.ranks.results
|
||||
total.value = res.data.ranks.total
|
||||
}
|
||||
|
||||
watch(() => query.page, init)
|
||||
watch(
|
||||
() => query.limit,
|
||||
() => {
|
||||
query.page = 1
|
||||
init()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(init)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical size="large">
|
||||
<n-h2>我的班级排名</n-h2>
|
||||
<n-alert v-if="className" type="info">
|
||||
班级:{{ className }} | 我的排名:{{
|
||||
myRank > 0 ? `第${myRank}名` : "暂无排名"
|
||||
}}
|
||||
| 班级总人数:{{ totalUsers }}
|
||||
</n-alert>
|
||||
<n-alert v-else type="warning"> 您还没有加入班级 </n-alert>
|
||||
|
||||
<n-data-table :data="data" :columns="columns" />
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="query.page"
|
||||
v-model:limit="query.limit"
|
||||
/>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getUserClassRank } from "oj/api"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { renderTableTitle } from "utils/renders"
|
||||
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 totalUsers = ref(0)
|
||||
const className = ref("")
|
||||
const data = ref<UserRank[]>([])
|
||||
const total = ref(0)
|
||||
const query = reactive({
|
||||
limit: 20,
|
||||
page: 1,
|
||||
})
|
||||
|
||||
const columns: DataTableColumn<UserRank>[] = [
|
||||
{
|
||||
title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"),
|
||||
key: "rank",
|
||||
width: 100,
|
||||
align: "center",
|
||||
render: (row) => {
|
||||
if (row.rank === 1) return "🥇"
|
||||
if (row.rank === 2) return "🥈"
|
||||
if (row.rank === 3) return "🥉"
|
||||
return row.rank
|
||||
},
|
||||
},
|
||||
{
|
||||
title: renderTableTitle(
|
||||
"用户名",
|
||||
"streamline-emojis:smiling-face-with-sunglasses",
|
||||
),
|
||||
key: "username",
|
||||
width: 200,
|
||||
render: (row) =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: "info",
|
||||
onClick: () => router.push("/user?name=" + row.username),
|
||||
},
|
||||
() => row.username,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: renderTableTitle("AC数", "streamline-emojis:raised-fist-1"),
|
||||
key: "accepted_number",
|
||||
width: 120,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: renderTableTitle("提交数", "streamline-emojis:rocket"),
|
||||
key: "submission_number",
|
||||
width: 120,
|
||||
align: "center",
|
||||
},
|
||||
]
|
||||
|
||||
async function init() {
|
||||
const user = userStore.user
|
||||
if (!user || !user.class_name) {
|
||||
message.warning("您没有班级信息")
|
||||
return
|
||||
}
|
||||
|
||||
const offset = (query.page - 1) * query.limit
|
||||
const res = await getUserClassRank(offset, query.limit)
|
||||
myRank.value = res.data.my_rank
|
||||
totalUsers.value = res.data.total_users
|
||||
className.value = res.data.class_name
|
||||
data.value = res.data.ranks.results
|
||||
total.value = res.data.ranks.total
|
||||
}
|
||||
|
||||
watch(() => query.page, init)
|
||||
watch(
|
||||
() => query.limit,
|
||||
() => {
|
||||
query.page = 1
|
||||
init()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(init)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical size="large">
|
||||
<n-h2>我的班级排名</n-h2>
|
||||
<n-alert v-if="className" type="info">
|
||||
班级:{{ className }} | 我的排名:{{
|
||||
myRank > 0 ? `第${myRank}名` : "暂无排名"
|
||||
}}
|
||||
| 班级总人数:{{ totalUsers }}
|
||||
</n-alert>
|
||||
<n-alert v-else type="warning"> 您还没有加入班级 </n-alert>
|
||||
|
||||
<n-data-table :data="data" :columns="columns" />
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="query.page"
|
||||
v-model:limit="query.limit"
|
||||
/>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
@@ -384,6 +384,14 @@ const radarChartData = computed(() => {
|
||||
|
||||
const datasets = comparisons.value.map((c, index) => {
|
||||
const color = getClassColor(index)
|
||||
const rawData = [
|
||||
c.total_ac,
|
||||
c.avg_ac,
|
||||
c.median_ac,
|
||||
c.excellent_rate,
|
||||
c.pass_rate,
|
||||
c.active_rate,
|
||||
]
|
||||
return {
|
||||
label: c.class_name,
|
||||
data: [
|
||||
@@ -394,6 +402,7 @@ const radarChartData = computed(() => {
|
||||
c.pass_rate,
|
||||
c.active_rate,
|
||||
],
|
||||
rawData,
|
||||
backgroundColor: color.bg,
|
||||
borderColor: color.border,
|
||||
borderWidth: 2,
|
||||
@@ -466,6 +475,28 @@ const radarChartOptions = {
|
||||
legend: {
|
||||
position: "bottom" as const,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context: any) {
|
||||
const dataset = context.dataset as any
|
||||
const rawValue = dataset?.rawData?.[context.dataIndex]
|
||||
const metric = context.label || ""
|
||||
const isRate = context.dataIndex >= 3
|
||||
if (rawValue === undefined || rawValue === null) {
|
||||
return `${dataset.label || ""}: ${context.parsed.r?.toFixed(2) ?? ""}`
|
||||
}
|
||||
const formatted = Number.isFinite(rawValue)
|
||||
? isRate
|
||||
? rawValue.toFixed(1)
|
||||
: Number.isInteger(rawValue)
|
||||
? rawValue.toString()
|
||||
: rawValue.toFixed(2)
|
||||
: String(rawValue)
|
||||
const suffix = isRate ? "%" : ""
|
||||
return `${dataset.label || ""} - ${metric}: ${formatted}${suffix}`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
r: {
|
||||
@@ -485,7 +516,10 @@ const radarChartOptions = {
|
||||
<n-h2 style="margin-bottom: 0">班级PK</n-h2>
|
||||
|
||||
<n-flex :wrap="false" align="flex-start" :size="16">
|
||||
<n-form-item label="选择班级(至少2个)" style="width: 300px; margin-bottom: 0">
|
||||
<n-form-item
|
||||
label="选择班级(至少2个)"
|
||||
style="width: 300px; margin-bottom: 0"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="selectedClasses"
|
||||
:options="classOptions"
|
||||
@@ -494,7 +528,10 @@ const radarChartOptions = {
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="时间段(可选)" style="width: 200px; margin-bottom: 0">
|
||||
<n-form-item
|
||||
label="时间段(可选)"
|
||||
style="width: 200px; margin-bottom: 0"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="duration"
|
||||
:options="timeRangeOptions"
|
||||
@@ -504,7 +541,12 @@ const radarChartOptions = {
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-button type="primary" @click="compare" :loading="loading" style="margin-top: 26px">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="compare"
|
||||
:loading="loading"
|
||||
style="margin-top: 26px"
|
||||
>
|
||||
开始PK
|
||||
</n-button>
|
||||
</n-flex>
|
||||
@@ -516,7 +558,10 @@ const radarChartOptions = {
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
>
|
||||
<n-gi v-for="(classData, index) in comparisons" :key="classData.class_name">
|
||||
<n-gi
|
||||
v-for="(classData, index) in comparisons"
|
||||
:key="classData.class_name"
|
||||
>
|
||||
<n-card
|
||||
:title="classData.class_name"
|
||||
:bordered="true"
|
||||
@@ -544,7 +589,12 @@ const radarChartOptions = {
|
||||
<!-- AC核心指标 - 突出显示,便于横向对比 -->
|
||||
<n-grid :cols="5" :x-gap="8" responsive="screen">
|
||||
<n-gi>
|
||||
<n-statistic label="总AC数" :value="classData.total_ac" size="large" class="stat-total-ac">
|
||||
<n-statistic
|
||||
label="总AC数"
|
||||
:value="classData.total_ac"
|
||||
size="large"
|
||||
class="stat-total-ac"
|
||||
>
|
||||
<template #suffix>
|
||||
<Icon icon="streamline-emojis:raised-fist-1" width="20" />
|
||||
</template>
|
||||
@@ -603,43 +653,71 @@ const radarChartOptions = {
|
||||
<n-divider style="margin: 12px 0" />
|
||||
|
||||
<!-- 详细统计 - 紧凑布局,统一格式 -->
|
||||
<n-descriptions bordered :column="2" size="small" label-placement="left">
|
||||
<n-descriptions
|
||||
bordered
|
||||
:column="2"
|
||||
size="small"
|
||||
label-placement="left"
|
||||
>
|
||||
<!-- 分位数统计 -->
|
||||
<n-descriptions-item label="第一四分位数(Q1)">
|
||||
<span style="color: #9254de; font-weight: 500">{{ classData.q1_ac.toFixed(2) }}</span>
|
||||
<span style="color: #9254de; font-weight: 500">{{
|
||||
classData.q1_ac.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="第三四分位数(Q3)">
|
||||
<span style="color: #f759ab; font-weight: 500">{{ classData.q3_ac.toFixed(2) }}</span>
|
||||
<span style="color: #f759ab; font-weight: 500">{{
|
||||
classData.q3_ac.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="四分位距(IQR)">
|
||||
<span style="color: #13c2c2; font-weight: 500">{{ classData.iqr.toFixed(2) }}</span>
|
||||
<span style="color: #13c2c2; font-weight: 500">{{
|
||||
classData.iqr.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="标准差">
|
||||
<span style="color: #fa8c16; font-weight: 500">{{ classData.std_dev.toFixed(2) }}</span>
|
||||
<span style="color: #fa8c16; font-weight: 500">{{
|
||||
classData.std_dev.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
|
||||
<!-- 分层统计 -->
|
||||
<n-descriptions-item label="前10名平均">
|
||||
<span style="color: #cf1322; font-weight: 600">{{ classData.top_10_avg.toFixed(2) }}</span>
|
||||
<span style="color: #cf1322; font-weight: 600">{{
|
||||
classData.top_10_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="后10名平均">
|
||||
<span style="color: #096dd9; font-weight: 500">{{ classData.bottom_10_avg.toFixed(2) }}</span>
|
||||
<span style="color: #096dd9; font-weight: 500">{{
|
||||
classData.bottom_10_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="前25%平均">
|
||||
<span style="color: #f5222d; font-weight: 600">{{ classData.top_25_avg.toFixed(2) }}</span>
|
||||
<span style="color: #f5222d; font-weight: 600">{{
|
||||
classData.top_25_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="后25%平均">
|
||||
<span style="color: #531dab; font-weight: 500">{{ classData.bottom_25_avg.toFixed(2) }}</span>
|
||||
<span style="color: #531dab; font-weight: 500">{{
|
||||
classData.bottom_25_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
|
||||
<!-- 人数 -->
|
||||
<n-descriptions-item label="人数">
|
||||
<span style="color: #1890ff; font-weight: 600">{{ classData.user_count }}</span>
|
||||
<span style="color: #1890ff; font-weight: 600">{{
|
||||
classData.user_count
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<!-- 比率统计 - 使用进度条图表 -->
|
||||
<n-card size="small" title="比率统计" embedded style="margin-top: 12px">
|
||||
<n-card
|
||||
size="small"
|
||||
title="比率统计"
|
||||
embedded
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
<n-space vertical :size="10">
|
||||
<n-progress
|
||||
type="line"
|
||||
@@ -680,22 +758,37 @@ const radarChartOptions = {
|
||||
<template
|
||||
v-if="hasTimeRange && classData.recent_total_ac !== undefined"
|
||||
>
|
||||
<n-descriptions bordered :column="2" size="small" label-placement="left" style="margin-top: 12px">
|
||||
|
||||
<n-descriptions
|
||||
bordered
|
||||
:column="2"
|
||||
size="small"
|
||||
label-placement="left"
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
<n-descriptions-item label="时间段总AC">
|
||||
<span style="color: #ff7875; font-weight: 600">{{ classData.recent_total_ac }}</span>
|
||||
<span style="color: #ff7875; font-weight: 600">{{
|
||||
classData.recent_total_ac
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="时间段平均AC">
|
||||
<span style="color: #73d13d; font-weight: 600">{{ classData.recent_avg_ac?.toFixed(2) }}</span>
|
||||
<span style="color: #73d13d; font-weight: 600">{{
|
||||
classData.recent_avg_ac?.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="时间段中位数AC">
|
||||
<span style="color: #ffc53d; font-weight: 600">{{ classData.recent_median_ac?.toFixed(2) }}</span>
|
||||
<span style="color: #ffc53d; font-weight: 600">{{
|
||||
classData.recent_median_ac?.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="时间段前10名平均">
|
||||
<span style="color: #ff4d4f; font-weight: 600">{{ classData.recent_top_10_avg?.toFixed(2) }}</span>
|
||||
<span style="color: #ff4d4f; font-weight: 600">{{
|
||||
classData.recent_top_10_avg?.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="活跃学生数" :span="2">
|
||||
<span style="color: #1890ff; font-weight: 600">{{ classData.recent_active_count }}</span>
|
||||
<span style="color: #1890ff; font-weight: 600">{{
|
||||
classData.recent_active_count
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</template>
|
||||
@@ -827,7 +920,11 @@ const radarChartOptions = {
|
||||
</template>
|
||||
|
||||
<!-- 对比表格 -->
|
||||
<n-card v-if="comparisons.length > 0" title="对比表格" style="margin-top: 20px">
|
||||
<n-card
|
||||
v-if="comparisons.length > 0"
|
||||
title="对比表格"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<n-data-table
|
||||
:data="comparisons"
|
||||
:columns="[
|
||||
@@ -842,48 +939,93 @@ const radarChartOptions = {
|
||||
title: '人数',
|
||||
key: 'user_count',
|
||||
width: 80,
|
||||
render: (row) => h('span', { style: { color: '#1890ff', fontWeight: '600' } }, row.user_count),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#1890ff', fontWeight: '600' } },
|
||||
row.user_count,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '总AC数',
|
||||
key: 'total_ac',
|
||||
width: 100,
|
||||
render: (row) => h('span', { style: { color: '#ff4d4f', fontWeight: '600' } }, row.total_ac),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#ff4d4f', fontWeight: '600' } },
|
||||
row.total_ac,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '平均AC',
|
||||
key: 'avg_ac',
|
||||
render: (row) => h('span', { style: { color: '#52c41a', fontWeight: '600' } }, row.avg_ac.toFixed(2)),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#52c41a', fontWeight: '600' } },
|
||||
row.avg_ac.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '中位数AC',
|
||||
key: 'median_ac',
|
||||
render: (row) => h('span', { style: { color: '#fa8c16', fontWeight: '600' } }, row.median_ac.toFixed(2)),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#fa8c16', fontWeight: '600' } },
|
||||
row.median_ac.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '前10名平均',
|
||||
key: 'top_10_avg',
|
||||
render: (row) => h('span', { style: { color: '#cf1322', fontWeight: '600' } }, row.top_10_avg.toFixed(2)),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#cf1322', fontWeight: '600' } },
|
||||
row.top_10_avg.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '后10名平均',
|
||||
key: 'bottom_10_avg',
|
||||
render: (row) => h('span', { style: { color: '#096dd9', fontWeight: '500' } }, row.bottom_10_avg.toFixed(2)),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#096dd9', fontWeight: '500' } },
|
||||
row.bottom_10_avg.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '优秀率',
|
||||
key: 'excellent_rate',
|
||||
render: (row) => h('span', { style: { color: '#faad14', fontWeight: '600' } }, row.excellent_rate.toFixed(1) + '%'),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#faad14', fontWeight: '600' } },
|
||||
row.excellent_rate.toFixed(1) + '%',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '及格率',
|
||||
key: 'pass_rate',
|
||||
render: (row) => h('span', { style: { color: '#52c41a', fontWeight: '600' } }, row.pass_rate.toFixed(1) + '%'),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#52c41a', fontWeight: '600' } },
|
||||
row.pass_rate.toFixed(1) + '%',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '参与度',
|
||||
key: 'active_rate',
|
||||
render: (row) => h('span', { style: { color: '#1890ff', fontWeight: '600' } }, row.active_rate.toFixed(1) + '%'),
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: '#1890ff', fontWeight: '600' } },
|
||||
row.active_rate.toFixed(1) + '%',
|
||||
),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
@@ -946,4 +1088,3 @@ const radarChartOptions = {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,144 +1,144 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -136,7 +136,11 @@ const menus = computed<MenuOption[]>(() => [
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(RouterLink, { to: "/class/my-rank" }, { default: () => "我的排名" }),
|
||||
h(
|
||||
RouterLink,
|
||||
{ to: "/class/my-rank" },
|
||||
{ default: () => "我的排名" },
|
||||
),
|
||||
key: "my-rank",
|
||||
show: userStore.isAuthed,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user