update yearly ac rate
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-05-11 00:43:11 -06:00
parent 8a6858d6de
commit 3d88ca22cb
4 changed files with 160 additions and 0 deletions

View File

@@ -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")
}

View 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>

View File

@@ -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">

View File

@@ -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",