diff --git a/src/oj/class/pk.vue b/src/oj/class/pk.vue index 8519fb7..982d25d 100644 --- a/src/oj/class/pk.vue +++ b/src/oj/class/pk.vue @@ -3,6 +3,36 @@ import { formatISO, sub, type Duration } from "date-fns" import { getClassPK } from "oj/api" import { useConfigStore } from "shared/store/config" import { Icon } from "@iconify/vue" +import { Bar, Radar } from "vue-chartjs" +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + RadialLinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + Colors, + Filler, +} from "chart.js" + +// 注册Chart.js组件 +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + RadialLinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + Colors, + Filler, +) const configStore = useConfigStore() const message = useMessage() @@ -37,7 +67,6 @@ const selectedClasses = ref([]) const comparisons = ref([]) const duration = ref("") const loading = ref(false) -const showDetails = ref>({}) const hasTimeRange = ref(false) // 时间段选项(与 rank/list.vue 保持一致) @@ -106,11 +135,6 @@ async function compare() { const res = await getClassPK(selectedClasses.value, startTime, endTime) comparisons.value = res.data.comparisons hasTimeRange.value = res.data.has_time_range || false - - // 初始化展开状态 - comparisons.value.forEach((c) => { - showDetails.value[c.class_name] = false - }) } catch (error) { message.error("获取数据失败") } finally { @@ -118,10 +142,6 @@ async function compare() { } } -function toggleDetails(class_name: string) { - showDetails.value[class_name] = !showDetails.value[class_name] -} - // 计算排名颜色 function getRankColor(index: number) { if (index === 0) return { type: "success" as const, text: "🥇" } @@ -129,14 +149,341 @@ function getRankColor(index: number) { if (index === 2) return { type: "warning" as const, text: "🥉" } return { type: "default" as const, text: `${index + 1}` } } + +// 获取班级颜色 +function getClassColor(index: number) { + const colors = [ + { bg: "rgba(24, 160, 88, 0.2)", border: "rgba(24, 160, 88, 0.8)" }, // success + { bg: "rgba(32, 128, 240, 0.2)", border: "rgba(32, 128, 240, 0.8)" }, // info + { bg: "rgba(240, 160, 32, 0.2)", border: "rgba(240, 160, 32, 0.8)" }, // warning + { bg: "rgba(208, 48, 80, 0.2)", border: "rgba(208, 48, 80, 0.8)" }, // error + { bg: "rgba(128, 90, 213, 0.2)", border: "rgba(128, 90, 213, 0.8)" }, // purple + { bg: "rgba(0, 184, 148, 0.2)", border: "rgba(0, 184, 148, 0.8)" }, // teal + ] + return colors[index % colors.length] +} + +// 总AC数对比图 - 每个班级用不同颜色 +const totalAcChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "总AC数", + data: comparisons.value.map((c) => c.total_ac), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 平均AC数对比图 +const avgAcChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "平均AC数", + data: comparisons.value.map((c) => c.avg_ac), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 中位数AC数对比图 +const medianAcChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "中位数AC数", + data: comparisons.value.map((c) => c.median_ac), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 优秀率对比图 +const excellentRateChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "优秀率", + data: comparisons.value.map((c) => c.excellent_rate), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 及格率对比图 +const passRateChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "及格率", + data: comparisons.value.map((c) => c.pass_rate), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 参与度对比图 +const activeRateChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "参与度", + data: comparisons.value.map((c) => c.active_rate), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 前10名平均对比图 +const top10AvgChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "前10名平均", + data: comparisons.value.map((c) => c.top_10_avg), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 后10名平均对比图 +const bottom10AvgChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "后10名平均", + data: comparisons.value.map((c) => c.bottom_10_avg), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 前25%平均对比图 +const top25AvgChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "前25%平均", + data: comparisons.value.map((c) => c.top_25_avg), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 后25%平均对比图 +const bottom25AvgChartData = computed(() => { + if (comparisons.value.length === 0) return null + + const labels = comparisons.value.map((c) => c.class_name) + const datasets = [ + { + label: "后25%平均", + data: comparisons.value.map((c) => c.bottom_25_avg), + backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg), + borderColor: comparisons.value.map((_, i) => getClassColor(i).border), + borderWidth: 2, + }, + ] + + return { labels, datasets } +}) + +// 雷达图数据 - 多维度综合对比 +const radarChartData = computed(() => { + if (comparisons.value.length === 0) return null + + // 归一化数据到0-100范围 + const normalize = (value: number, max: number, min: number) => { + if (max === min) return 50 + return ((value - min) / (max - min)) * 100 + } + + const metrics = [ + "总AC数", + "平均AC数", + "中位数AC数", + "优秀率", + "及格率", + "参与度", + ] + + // 计算每个指标的最大最小值 + const maxValues = [ + Math.max(...comparisons.value.map((c) => c.total_ac)), + Math.max(...comparisons.value.map((c) => c.avg_ac)), + Math.max(...comparisons.value.map((c) => c.median_ac)), + 100, // 优秀率最大值 + 100, // 及格率最大值 + 100, // 参与度最大值 + ] + + const minValues = [ + Math.min(...comparisons.value.map((c) => c.total_ac)), + Math.min(...comparisons.value.map((c) => c.avg_ac)), + Math.min(...comparisons.value.map((c) => c.median_ac)), + 0, + 0, + 0, + ] + + const datasets = comparisons.value.map((c, index) => { + const color = getClassColor(index) + return { + label: c.class_name, + data: [ + normalize(c.total_ac, maxValues[0], minValues[0]), + normalize(c.avg_ac, maxValues[1], minValues[1]), + normalize(c.median_ac, maxValues[2], minValues[2]), + c.excellent_rate, + c.pass_rate, + c.active_rate, + ], + backgroundColor: color.bg, + borderColor: color.border, + borderWidth: 2, + pointBackgroundColor: color.border, + pointBorderColor: "#fff", + pointHoverBackgroundColor: "#fff", + pointHoverBorderColor: color.border, + } + }) + + return { + labels: metrics, + datasets, + } +}) + +// 图表配置 - 优化对比效果 +const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "bottom" as const, + display: true, + labels: { + boxWidth: 0, + padding: 10, + }, + }, + tooltip: { + mode: "index" as const, + intersect: false, + callbacks: { + label: function (context: any) { + let label = context.dataset.label || "" + if (label) { + label += ": " + } + if (context.parsed.y !== null) { + label += context.parsed.y.toFixed(2) + } + return label + }, + }, + }, + datalabels: { + display: false, + }, + }, + scales: { + y: { + beginAtZero: true, + grid: { + display: true, + color: "rgba(0, 0, 0, 0.05)", + }, + }, + x: { + grid: { + display: false, + }, + }, + }, +} + +const radarChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "bottom" as const, + }, + }, + scales: { + r: { + beginAtZero: true, + max: 100, + ticks: { + stepSize: 20, + }, + }, + }, +} - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - {{ showDetails[classData.class_name] ? "收起" : "展开" }}详细统计 - - - - - - + + + - - - {{ classData.q1_ac.toFixed(2) }} - - - {{ classData.q3_ac.toFixed(2) }} - - - {{ classData.iqr.toFixed(2) }} - - - {{ classData.std_dev.toFixed(2) }} - - - - + + {{ classData.q1_ac.toFixed(2) }} + + + {{ classData.q3_ac.toFixed(2) }} + + + {{ classData.iqr.toFixed(2) }} + + + {{ classData.std_dev.toFixed(2) }} + - 分层统计 - - - {{ classData.top_10_avg.toFixed(2) }} - - - {{ classData.bottom_10_avg.toFixed(2) }} - - - {{ classData.top_25_avg.toFixed(2) }} - - - {{ classData.bottom_25_avg.toFixed(2) }} - - + + {{ classData.top_10_avg.toFixed(2) }} + + + {{ classData.bottom_10_avg.toFixed(2) }} + + + {{ classData.top_25_avg.toFixed(2) }} + + + {{ classData.bottom_25_avg.toFixed(2) }} + - + + + {{ classData.user_count }} + + - - 比率统计 - + + + + - - - + + + + +