Files
ojnext/src/oj/rank/list.vue
2025-10-05 20:07:41 +08:00

176 lines
4.4 KiB
Vue

<script setup lang="ts">
import { formatISO, sub, type Duration } from "date-fns"
import { NButton } from "naive-ui"
import { getActivityRank, getRank } from "oj/api"
import { getACRate } from "utils/functions"
import { Rank } from "utils/types"
import Pagination from "shared/components/Pagination.vue"
import { ChartType } from "utils/constants"
import { renderTableTitle } from "utils/renders"
import Chart from "./components/Chart.vue"
import Index from "./components/Index.vue"
const router = useRouter()
const data = ref<Rank[]>([])
const total = ref(0)
const query = reactive({
limit: 10,
page: 1,
})
const chart = ref<Rank[]>([])
const chartType = ref(ChartType.Rank)
const duration = ref("weeks:1")
async function init() {
const offset = (query.page - 1) * query.limit
const res = await getRank(offset, query.limit, 100)
data.value = res.data.results
total.value = res.data.total
return res.data.results
}
const columns: DataTableColumn<Rank>[] = [
{
title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"),
key: "index",
width: 100,
align: "center",
render: (_, index) =>
h(Index, { index, page: query.page, limit: query.limit }),
},
{
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.user.username),
},
() => row.user.username,
),
},
{
title: renderTableTitle(
"个性签名",
"streamline-emojis:no-one-under-eighteen",
),
key: "mood",
minWidth: 200,
},
{
title: renderTableTitle("已解决", "streamline-emojis:raised-fist-1"),
key: "accepted_number",
width: 120,
align: "center",
},
{
title: renderTableTitle("提交数", "streamline-emojis:rocket"),
key: "submission_number",
width: 120,
align: "center",
},
{
title: renderTableTitle("正确率", "streamline-emojis:wrapped-gift-1"),
key: "rate",
width: 120,
align: "center",
render: (row) => getACRate(row.accepted_number, row.submission_number),
},
]
watch(() => query.page, init)
watch(
() => query.limit,
() => {
query.page = 1
init()
},
)
watch(duration, listActivity)
async function listActivity() {
chartType.value = ChartType.Activity
const current = Date.now()
const start = formatISO(sub(current, subOptions.value))
const res = await getActivityRank(start)
chart.value = res.data.map((d: { username: string; count: number }) => ({
user: {
username: d.username,
},
accepted_number: d.count,
submission_number: 0,
}))
}
async function listRank() {
chartType.value = ChartType.Rank
const res = await getRank(0, 10, 10)
data.value = res.data.results
chart.value = data.value
}
const options: SelectOption[] = [
{ label: "一周内", value: "weeks:1" },
{ label: "一个月内", value: "months:1" },
{ label: "两个月内", value: "months:2" },
{ label: "半年内", value: "months:6" },
{ label: "一年内", value: "years:1" },
]
const subOptions = computed<Duration>(() => {
let dur = options.find((it) => it.value === duration.value) ?? options[0]
const x = dur.value!.toString().split(":")
const unit = x[0]
const n = x[1]
return { [unit]: parseInt(n) }
})
onMounted(async () => {
chart.value = await init()
})
</script>
<template>
<n-flex justify="center">
<n-button-group>
<n-button
@click="listRank"
:type="chartType === ChartType.Rank ? 'primary' : 'default'"
>
天梯排名
</n-button>
<n-button
@click="listActivity"
:type="chartType === ChartType.Activity ? 'primary' : 'default'"
>
活跃度排名
</n-button>
</n-button-group>
<div v-if="chartType === ChartType.Activity">
<n-select
style="width: 120px"
:options="options"
v-model:value="duration"
/>
</div>
</n-flex>
<Chart v-if="!!chart.length" :type="chartType" :rank-data="chart" />
<n-empty v-else style="padding: 20px 0"></n-empty>
<n-flex justify="center">
<n-h2>全校前100名</n-h2>
</n-flex>
<n-data-table :data="data" :columns="columns" />
<Pagination
:total="total"
v-model:page="query.page"
v-model:limit="query.limit"
/>
</template>