update yearly ac rate
This commit is contained in:
@@ -473,3 +473,7 @@ export function removeUserFromProblemSet(problemSetId: number, userId: number) {
|
|||||||
export function getStuckProblems() {
|
export function getStuckProblems() {
|
||||||
return http.get("admin/problem/stuck")
|
return http.get("admin/problem/stuck")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTopACTrend() {
|
||||||
|
return http.get("admin/problem/top_ac_trend")
|
||||||
|
}
|
||||||
|
|||||||
144
src/admin/problem/TopACTrend.vue
Normal file
144
src/admin/problem/TopACTrend.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Line } from "vue-chartjs"
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "chart.js"
|
||||||
|
import { getTopACTrend } from "admin/api"
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface YearlyEntry {
|
||||||
|
year: number
|
||||||
|
total: number
|
||||||
|
accepted: number
|
||||||
|
ac_rate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProblemTrend {
|
||||||
|
problem_id: string
|
||||||
|
problem_title: string
|
||||||
|
yearly: YearlyEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
"#4e79a7",
|
||||||
|
"#f28e2b",
|
||||||
|
"#e15759",
|
||||||
|
"#76b7b2",
|
||||||
|
"#59a14f",
|
||||||
|
"#edc948",
|
||||||
|
"#b07aa1",
|
||||||
|
"#ff9da7",
|
||||||
|
"#9c755f",
|
||||||
|
"#bab0ac",
|
||||||
|
]
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const data = ref<ProblemTrend[]>([])
|
||||||
|
|
||||||
|
const allYears = computed(() => {
|
||||||
|
const years = new Set<number>()
|
||||||
|
data.value.forEach((p) => p.yearly.forEach((y) => years.add(y.year)))
|
||||||
|
return Array.from(years).sort()
|
||||||
|
})
|
||||||
|
|
||||||
|
const chartData = computed(() => ({
|
||||||
|
labels: allYears.value.map(String),
|
||||||
|
datasets: data.value.map((problem, i) => {
|
||||||
|
const byYear = new Map(problem.yearly.map((y) => [y.year, y]))
|
||||||
|
return {
|
||||||
|
label: `${problem.problem_id} ${problem.problem_title}`,
|
||||||
|
data: allYears.value.map((year) => byYear.get(year)?.ac_rate ?? null),
|
||||||
|
borderColor: COLORS[i % COLORS.length],
|
||||||
|
backgroundColor: COLORS[i % COLORS.length] + "33",
|
||||||
|
tension: 0.3,
|
||||||
|
spanGaps: false,
|
||||||
|
pointRadius: 4,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const chartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "提交次数前 10 题目 · 历年 AC 率",
|
||||||
|
font: { size: 18 },
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: "bottom" as const,
|
||||||
|
labels: { boxWidth: 12, padding: 12 },
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx: any) => {
|
||||||
|
const problem = data.value[ctx.datasetIndex]
|
||||||
|
const year = allYears.value[ctx.dataIndex]
|
||||||
|
const entry = problem.yearly.find((y) => y.year === year)
|
||||||
|
if (!entry) return `${ctx.dataset.label}: 无数据`
|
||||||
|
return `${ctx.dataset.label}: ${entry.ac_rate}% (${entry.accepted}/${entry.total})`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
ticks: { callback: (v: any) => `${v}%` },
|
||||||
|
title: { display: true, text: "AC 率" },
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: { display: true, text: "年份" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const res = await getTopACTrend()
|
||||||
|
data.value = res.data
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h2 style="margin-top: 0">提交次数前 10 题目 · 历年 AC 率趋势</h2>
|
||||||
|
<n-spin :show="loading">
|
||||||
|
<div v-if="!loading && data.length === 0" style="text-align: center; padding: 40px">
|
||||||
|
暂无数据
|
||||||
|
</div>
|
||||||
|
<div v-else class="chart-wrapper">
|
||||||
|
<Line :data="chartData" :options="chartOptions" />
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -146,6 +146,12 @@ watch(() => [query.page, query.limit, query.author], listProblems)
|
|||||||
>
|
>
|
||||||
卡点分析
|
卡点分析
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
v-if="!isContestProblemList"
|
||||||
|
@click="$router.push({ name: 'admin top ac trend' })"
|
||||||
|
>
|
||||||
|
年度趋势
|
||||||
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<n-button v-if="isContestProblemList" @click="createContestProblem">
|
<n-button v-if="isContestProblemList" @click="createContestProblem">
|
||||||
|
|||||||
@@ -282,6 +282,12 @@ export const admins: RouteRecordRaw = {
|
|||||||
component: () => import("admin/problem/Stuck.vue"),
|
component: () => import("admin/problem/Stuck.vue"),
|
||||||
meta: { requiresSuperAdmin: true },
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "problem/top_ac_trend",
|
||||||
|
name: "admin top ac trend",
|
||||||
|
component: () => import("admin/problem/TopACTrend.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
|
},
|
||||||
// 题单管理路由
|
// 题单管理路由
|
||||||
{
|
{
|
||||||
path: "problemset/list",
|
path: "problemset/list",
|
||||||
|
|||||||
Reference in New Issue
Block a user