feat(卡点分析): 「卡住」原来数的是错过一次的人,做的人多的题全排在前面;改成看一次就对、交几次才对,能按班级和时间段筛
- 卡点按「人 × 题」算:一次就对、做对之前交了几次、还没做对几人,带知识点;一道题给每个人的情况、错在哪、全站历年一次就对 - 年度趋势换成历年概况:学年按 9 月切(原来按自然年),进行中的学年和去年同期并排 - 删掉旧的 /problem-analytics/stuck 全站前 40 和 /ac-trend Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e23d28f62
commit
b2c802174c
6 files changed
+465
-355
No files matched your search
@@ -1,189 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import type { AcTrend } from "utils/types"
|
||||
import { Line } from "vue-chartjs"
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
Filler,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "chart.js"
|
||||
import { getTopACTrend } from "admin/api"
|
||||
import { zonedYear } from "utils/functions"
|
||||
|
||||
ChartJS.register(CategoryScale, Filler, LinearScale, LineElement, PointElement, Title, Tooltip)
|
||||
|
||||
type ProblemTrend = AcTrend
|
||||
|
||||
// 年份按东八区取,和后端 ac-trend 的夹逼口径(`localYear()`)对齐。
|
||||
// 用 `new Date().getFullYear()` 的话,跨年那几个小时里浏览器年份可能比后端认定的
|
||||
// 年份晚一年,默认的 untilYear 会被后端夹掉、图表悄悄变成另一个区间。
|
||||
const currentYear = zonedYear()
|
||||
const yearOptions = Array.from({ length: currentYear - 2022 + 1 }, (_, i) => ({
|
||||
label: String(2022 + i),
|
||||
value: 2022 + i,
|
||||
}))
|
||||
const minPerYearOptions = [
|
||||
{ label: "50", value: 50 },
|
||||
{ label: "100", value: 100 },
|
||||
{ label: "200", value: 200 },
|
||||
]
|
||||
|
||||
const sinceYear = ref(2023)
|
||||
const untilYear = ref(currentYear - 1)
|
||||
const minPerYear = ref(100)
|
||||
const loading = ref(false)
|
||||
const data = ref<ProblemTrend[]>([])
|
||||
|
||||
const acLabelPlugin = {
|
||||
id: "acLabel",
|
||||
afterDatasetsDraw(chart: any) {
|
||||
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()
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
function getChartData(problem: ProblemTrend) {
|
||||
return {
|
||||
labels: problem.yearly.map((y) => String(y.year)),
|
||||
datasets: [
|
||||
{
|
||||
label: "AC 率",
|
||||
data: problem.yearly.map((y) => y.acRate),
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
backgroundColor: "rgba(99, 179, 237, 0.2)",
|
||||
borderColor: "rgba(99, 179, 237, 1)",
|
||||
pointBackgroundColor: "rgba(99, 179, 237, 1)",
|
||||
pointRadius: 4,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function getChartOptions(problem: ProblemTrend) {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `${problem.problemId} · ${problem.problemTitle}`,
|
||||
font: { size: 14 },
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (ctx: any) => {
|
||||
const entry = problem.yearly[ctx.dataIndex]
|
||||
return `AC 率: ${entry.acRate}% (${entry.accepted}/${entry.total})`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
ticks: { callback: (v: any) => `${v}%` },
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: "年份" },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getTopACTrend({
|
||||
sinceYear: sinceYear.value,
|
||||
untilYear: untilYear.value,
|
||||
minPerYear: minPerYear.value,
|
||||
})
|
||||
data.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader title="年度趋势">
|
||||
<template #extra>
|
||||
<n-tag type="info" size="small" round>共 {{ data.length }} 题</n-tag>
|
||||
</template>
|
||||
<template #filters>
|
||||
<span>年份范围</span>
|
||||
<n-select
|
||||
v-model:value="sinceYear"
|
||||
:options="yearOptions"
|
||||
style="width: 100px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
<span>—</span>
|
||||
<n-select
|
||||
v-model:value="untilYear"
|
||||
:options="yearOptions"
|
||||
style="width: 100px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
<span>年提交下限</span>
|
||||
<n-select
|
||||
v-model:value="minPerYear"
|
||||
:options="minPerYearOptions"
|
||||
style="width: 90px"
|
||||
@update:value="fetchData"
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-spin :show="loading">
|
||||
<div v-if="!loading && data.length === 0" style="text-align: center; padding: 40px">
|
||||
暂无数据
|
||||
</div>
|
||||
<div v-else class="grid">
|
||||
<div v-for="problem in data" :key="problem.problemId" class="chart-card">
|
||||
<Line
|
||||
:data="getChartData(problem)"
|
||||
:options="getChartOptions(problem)"
|
||||
:plugins="[acLabelPlugin]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-spin>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
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>
|
||||
Reference in new issue
Block a user