update
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:55:04 -06:00
parent 3d88ca22cb
commit c5a89c6d6a

View File

@@ -4,7 +4,6 @@ import {
Chart as ChartJS, Chart as ChartJS,
CategoryScale, CategoryScale,
Filler, Filler,
Legend,
LinearScale, LinearScale,
LineElement, LineElement,
PointElement, PointElement,
@@ -16,7 +15,6 @@ import { getTopACTrend } from "admin/api"
ChartJS.register( ChartJS.register(
CategoryScale, CategoryScale,
Filler, Filler,
Legend,
LinearScale, LinearScale,
LineElement, LineElement,
PointElement, PointElement,
@@ -37,80 +35,78 @@ interface ProblemTrend {
yearly: YearlyEntry[] yearly: YearlyEntry[]
} }
const COLORS = [
"#4e79a7",
"#f28e2b",
"#e15759",
"#76b7b2",
"#59a14f",
"#edc948",
"#b07aa1",
"#ff9da7",
"#9c755f",
"#bab0ac",
]
const loading = ref(true) const loading = ref(true)
const data = ref<ProblemTrend[]>([]) const data = ref<ProblemTrend[]>([])
const allYears = computed(() => { const acLabelPlugin = {
const years = new Set<number>() id: "acLabel",
data.value.forEach((p) => p.yearly.forEach((y) => years.add(y.year))) afterDatasetsDraw(chart: any) {
return Array.from(years).sort() const ctx = chart.ctx
}) chart.data.datasets.forEach((_: any, i: number) => {
const meta = chart.getDatasetMeta(i)
meta.data.forEach((point: any, j: number) => {
const value = chart.data.datasets[i].data[j]
if (value === null || value === undefined) return
ctx.save()
ctx.font = "bold 11px sans-serif"
ctx.fillStyle = "rgba(99, 179, 237, 1)"
ctx.textAlign = "center"
ctx.textBaseline = "bottom"
ctx.fillText(`${value}%`, point.x, point.y - 6)
ctx.restore()
})
})
},
}
const chartData = computed(() => ({ function getChartData(problem: ProblemTrend) {
labels: allYears.value.map(String), return {
datasets: data.value.map((problem, i) => { labels: problem.yearly.map((y) => String(y.year)),
const byYear = new Map(problem.yearly.map((y) => [y.year, y])) datasets: [
return { {
label: `${problem.problem_id} ${problem.problem_title}`, label: "AC 率",
data: allYears.value.map((year) => byYear.get(year)?.ac_rate ?? null), data: problem.yearly.map((y) => y.ac_rate),
borderColor: COLORS[i % COLORS.length], fill: true,
backgroundColor: COLORS[i % COLORS.length] + "33", tension: 0.3,
tension: 0.3, backgroundColor: "rgba(99, 179, 237, 0.2)",
spanGaps: false, borderColor: "rgba(99, 179, 237, 1)",
pointRadius: 4, pointBackgroundColor: "rgba(99, 179, 237, 1)",
} pointRadius: 4,
}), },
})) ],
}
}
const chartOptions = { function getChartOptions(problem: ProblemTrend) {
responsive: true, return {
maintainAspectRatio: false, responsive: true,
plugins: { maintainAspectRatio: false,
title: { plugins: {
display: true, title: {
text: "提交次数前 10 题目 · 历年 AC 率", display: true,
font: { size: 18 }, text: `${problem.problem_id} · ${problem.problem_title}`,
}, font: { size: 14 },
legend: { },
position: "bottom" as const, tooltip: {
labels: { boxWidth: 12, padding: 12 }, callbacks: {
}, label: (ctx: any) => {
tooltip: { const entry = problem.yearly[ctx.dataIndex]
callbacks: { return `AC 率: ${entry.ac_rate}% (${entry.accepted}/${entry.total})`
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: {
scales: { y: {
y: { min: 0,
min: 0, max: 100,
max: 100, ticks: { callback: (v: any) => `${v}%` },
ticks: { callback: (v: any) => `${v}%` }, },
title: { display: true, text: "AC 率" }, x: {
title: { display: true, text: "年份" },
},
}, },
x: { }
title: { display: true, text: "年份" },
},
},
} }
onMounted(async () => { onMounted(async () => {
@@ -124,21 +120,34 @@ onMounted(async () => {
</script> </script>
<template> <template>
<h2 style="margin-top: 0">提交次数前 10 题目 · 历年 AC 趋势</h2> <h2 style="margin-top: 0">年度趋势</h2>
<n-spin :show="loading"> <n-spin :show="loading">
<div v-if="!loading && data.length === 0" style="text-align: center; padding: 40px"> <div
v-if="!loading && data.length === 0"
style="text-align: center; padding: 40px"
>
暂无数据 暂无数据
</div> </div>
<div v-else class="chart-wrapper"> <div v-else class="grid">
<Line :data="chartData" :options="chartOptions" /> <div v-for="problem in data" :key="problem.problem_id" class="chart-card">
<Line :data="getChartData(problem)" :options="getChartOptions(problem)" :plugins="[acLabelPlugin]" />
</div>
</div> </div>
</n-spin> </n-spin>
</template> </template>
<style scoped> <style scoped>
.chart-wrapper { .grid {
width: 100%; display: grid;
height: 500px; grid-template-columns: repeat(2, 1fr);
padding: 16px 0; gap: 24px;
padding: 8px 0;
}
.chart-card {
height: 260px;
border-radius: 8px;
padding: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
} }
</style> </style>