@@ -3,6 +3,36 @@ import { formatISO, sub, type Duration } from "date-fns"
|
|||||||
import { getClassPK } from "oj/api"
|
import { getClassPK } from "oj/api"
|
||||||
import { useConfigStore } from "shared/store/config"
|
import { useConfigStore } from "shared/store/config"
|
||||||
import { Icon } from "@iconify/vue"
|
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 configStore = useConfigStore()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -37,7 +67,6 @@ const selectedClasses = ref<string[]>([])
|
|||||||
const comparisons = ref<ClassComparison[]>([])
|
const comparisons = ref<ClassComparison[]>([])
|
||||||
const duration = ref<string>("")
|
const duration = ref<string>("")
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const showDetails = ref<Record<string, boolean>>({})
|
|
||||||
const hasTimeRange = ref(false)
|
const hasTimeRange = ref(false)
|
||||||
|
|
||||||
// 时间段选项(与 rank/list.vue 保持一致)
|
// 时间段选项(与 rank/list.vue 保持一致)
|
||||||
@@ -106,11 +135,6 @@ async function compare() {
|
|||||||
const res = await getClassPK(selectedClasses.value, startTime, endTime)
|
const res = await getClassPK(selectedClasses.value, startTime, endTime)
|
||||||
comparisons.value = res.data.comparisons
|
comparisons.value = res.data.comparisons
|
||||||
hasTimeRange.value = res.data.has_time_range || false
|
hasTimeRange.value = res.data.has_time_range || false
|
||||||
|
|
||||||
// 初始化展开状态
|
|
||||||
comparisons.value.forEach((c) => {
|
|
||||||
showDetails.value[c.class_name] = false
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error("获取数据失败")
|
message.error("获取数据失败")
|
||||||
} finally {
|
} 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) {
|
function getRankColor(index: number) {
|
||||||
if (index === 0) return { type: "success" as const, text: "🥇" }
|
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: "🥉" }
|
if (index === 2) return { type: "warning" as const, text: "🥉" }
|
||||||
return { type: "default" as const, text: `${index + 1}` }
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card>
|
<n-card>
|
||||||
<n-flex vertical :size="20">
|
<n-flex vertical :size="20">
|
||||||
<n-h2>班级PK</n-h2>
|
<n-h2 style="margin-bottom: 0">班级PK</n-h2>
|
||||||
|
|
||||||
<n-flex :wrap="false" align="center" :size="16">
|
<n-flex :wrap="false" align="flex-start" :size="16">
|
||||||
<n-form-item label="选择班级(至少2个)" style="width: 300px; margin-bottom: 0">
|
<n-form-item label="选择班级(至少2个)" style="width: 300px; margin-bottom: 0">
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="selectedClasses"
|
v-model:value="selectedClasses"
|
||||||
@@ -156,7 +503,7 @@ function getRankColor(index: number) {
|
|||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-button type="primary" @click="compare" :loading="loading">
|
<n-button type="primary" @click="compare" :loading="loading" style="margin-top: 26px">
|
||||||
开始PK
|
开始PK
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
@@ -191,52 +538,68 @@ function getRankColor(index: number) {
|
|||||||
</n-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 基础统计 -->
|
<!-- 班级信息布局 - 优化为便于比较 -->
|
||||||
<n-flex vertical :size="12">
|
<n-flex vertical :size="12">
|
||||||
<n-statistic label="总AC数" :value="classData.total_ac">
|
<!-- AC核心指标 - 突出显示,便于横向对比 -->
|
||||||
|
<n-grid :cols="5" :x-gap="8" responsive="screen">
|
||||||
|
<n-gi>
|
||||||
|
<n-statistic label="总AC数" :value="classData.total_ac" size="large">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<Icon icon="streamline-emojis:raised-fist-1" width="20" />
|
<Icon icon="streamline-emojis:raised-fist-1" width="20" />
|
||||||
</template>
|
</template>
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
<n-statistic label="平均AC数" :value="classData.avg_ac.toFixed(2)">
|
<n-gi>
|
||||||
|
<n-statistic
|
||||||
|
label="平均AC数"
|
||||||
|
:value="classData.avg_ac.toFixed(2)"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<Icon icon="streamline-emojis:chart" width="20" />
|
<Icon icon="streamline-emojis:chart" width="20" />
|
||||||
</template>
|
</template>
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="中位数AC数"
|
label="中位数AC数"
|
||||||
:value="classData.median_ac.toFixed(2)"
|
:value="classData.median_ac.toFixed(2)"
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<Icon icon="streamline-emojis:target" width="20" />
|
<Icon icon="streamline-emojis:target" width="20" />
|
||||||
</template>
|
</template>
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
<!-- 展开详细统计 -->
|
<n-gi>
|
||||||
<n-button
|
<n-statistic
|
||||||
text
|
label="总提交数"
|
||||||
@click="toggleDetails(classData.class_name)"
|
:value="classData.total_submission"
|
||||||
style="margin-top: 8px"
|
size="large"
|
||||||
>
|
>
|
||||||
{{ showDetails[classData.class_name] ? "收起" : "展开" }}详细统计
|
<template #suffix>
|
||||||
<Icon
|
<Icon icon="streamline-emojis:paper" width="20" />
|
||||||
:icon="
|
</template>
|
||||||
showDetails[classData.class_name]
|
</n-statistic>
|
||||||
? 'mdi:chevron-up'
|
</n-gi>
|
||||||
: 'mdi:chevron-down'
|
<n-gi>
|
||||||
"
|
<n-statistic
|
||||||
width="16"
|
label="AC率"
|
||||||
/>
|
:value="classData.ac_rate.toFixed(1) + '%'"
|
||||||
</n-button>
|
size="large"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<Icon icon="streamline-emojis:check-mark" width="20" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
<!-- 详细统计面板 -->
|
<n-divider style="margin: 12px 0" />
|
||||||
<n-collapse-transition :show="showDetails[classData.class_name]">
|
|
||||||
<n-divider />
|
|
||||||
|
|
||||||
|
<!-- 详细统计 - 紧凑布局,统一格式 -->
|
||||||
|
<n-descriptions bordered :column="2" size="small" label-placement="left">
|
||||||
<!-- 分位数统计 -->
|
<!-- 分位数统计 -->
|
||||||
<n-descriptions bordered :column="2" size="small">
|
|
||||||
<n-descriptions-item label="第一四分位数(Q1)">
|
<n-descriptions-item label="第一四分位数(Q1)">
|
||||||
{{ classData.q1_ac.toFixed(2) }}
|
{{ classData.q1_ac.toFixed(2) }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
@@ -249,13 +612,8 @@ function getRankColor(index: number) {
|
|||||||
<n-descriptions-item label="标准差">
|
<n-descriptions-item label="标准差">
|
||||||
{{ classData.std_dev.toFixed(2) }}
|
{{ classData.std_dev.toFixed(2) }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
|
||||||
|
|
||||||
<n-divider />
|
|
||||||
|
|
||||||
<!-- 分层统计 -->
|
<!-- 分层统计 -->
|
||||||
<n-h4 style="margin: 12px 0 8px 0">分层统计</n-h4>
|
|
||||||
<n-descriptions bordered :column="2" size="small">
|
|
||||||
<n-descriptions-item label="前10名平均">
|
<n-descriptions-item label="前10名平均">
|
||||||
{{ classData.top_10_avg.toFixed(2) }}
|
{{ classData.top_10_avg.toFixed(2) }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
@@ -268,13 +626,16 @@ function getRankColor(index: number) {
|
|||||||
<n-descriptions-item label="后25%平均">
|
<n-descriptions-item label="后25%平均">
|
||||||
{{ classData.bottom_25_avg.toFixed(2) }}
|
{{ classData.bottom_25_avg.toFixed(2) }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
|
|
||||||
|
<!-- 人数 -->
|
||||||
|
<n-descriptions-item label="人数">
|
||||||
|
{{ classData.user_count }}
|
||||||
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
|
|
||||||
<n-divider />
|
<!-- 比率统计 - 使用进度条图表 -->
|
||||||
|
<n-card size="small" title="比率统计" embedded style="margin-top: 12px">
|
||||||
<!-- 比率统计 -->
|
<n-space vertical :size="10">
|
||||||
<n-h4 style="margin: 12px 0 8px 0">比率统计</n-h4>
|
|
||||||
<n-space vertical :size="8">
|
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="classData.excellent_rate"
|
:percentage="classData.excellent_rate"
|
||||||
@@ -308,14 +669,14 @@ function getRankColor(index: number) {
|
|||||||
</template>
|
</template>
|
||||||
</n-progress>
|
</n-progress>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
<!-- 时间段统计(如果有) -->
|
<!-- 时间段统计(如果有) -->
|
||||||
<template
|
<template
|
||||||
v-if="hasTimeRange && classData.recent_total_ac !== undefined"
|
v-if="hasTimeRange && classData.recent_total_ac !== undefined"
|
||||||
>
|
>
|
||||||
<n-divider />
|
<n-descriptions bordered :column="2" size="small" label-placement="left" style="margin-top: 12px">
|
||||||
<n-h4 style="margin: 12px 0 8px 0">时间段内表现</n-h4>
|
|
||||||
<n-descriptions bordered :column="2" size="small">
|
|
||||||
<n-descriptions-item label="时间段总AC">
|
<n-descriptions-item label="时间段总AC">
|
||||||
{{ classData.recent_total_ac }}
|
{{ classData.recent_total_ac }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
@@ -328,17 +689,138 @@ function getRankColor(index: number) {
|
|||||||
<n-descriptions-item label="时间段前10名平均">
|
<n-descriptions-item label="时间段前10名平均">
|
||||||
{{ classData.recent_top_10_avg?.toFixed(2) }}
|
{{ classData.recent_top_10_avg?.toFixed(2) }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="活跃学生数">
|
<n-descriptions-item label="活跃学生数" :span="2">
|
||||||
{{ classData.recent_active_count }}
|
{{ classData.recent_active_count }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
</template>
|
</template>
|
||||||
</n-collapse-transition>
|
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|
||||||
|
<!-- 可视化图表 - 专注于对比 -->
|
||||||
|
<template v-if="comparisons.length > 0">
|
||||||
|
<!-- AC核心指标对比 - 三个独立图表并排显示 -->
|
||||||
|
<n-card title="AC核心指标对比" style="margin-top: 20px">
|
||||||
|
<n-grid :cols="3" :x-gap="16" :y-gap="16">
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="totalAcChartData"
|
||||||
|
:data="totalAcChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="avgAcChartData"
|
||||||
|
:data="avgAcChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="medianAcChartData"
|
||||||
|
:data="medianAcChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 比率统计对比 - 三个独立图表并排显示 -->
|
||||||
|
<n-card title="比率统计对比" style="margin-top: 20px">
|
||||||
|
<n-grid :cols="3" :x-gap="16" :y-gap="16">
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="excellentRateChartData"
|
||||||
|
:data="excellentRateChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="passRateChartData"
|
||||||
|
:data="passRateChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="activeRateChartData"
|
||||||
|
:data="activeRateChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 分层统计对比 - 四个独立图表并排显示 -->
|
||||||
|
<n-card title="分层统计对比" style="margin-top: 20px">
|
||||||
|
<n-grid :cols="2" :x-gap="16" :y-gap="16">
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="top10AvgChartData"
|
||||||
|
:data="top10AvgChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="bottom10AvgChartData"
|
||||||
|
:data="bottom10AvgChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="top25AvgChartData"
|
||||||
|
:data="top25AvgChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<div style="height: 300px">
|
||||||
|
<Bar
|
||||||
|
v-if="bottom25AvgChartData"
|
||||||
|
:data="bottom25AvgChartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 多维度雷达图 - 综合对比 -->
|
||||||
|
<n-card title="多维度综合对比" style="margin-top: 20px">
|
||||||
|
<div style="height: 500px">
|
||||||
|
<Radar
|
||||||
|
v-if="radarChartData"
|
||||||
|
:data="radarChartData"
|
||||||
|
:options="radarChartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 对比表格 -->
|
<!-- 对比表格 -->
|
||||||
<n-card v-if="comparisons.length > 0" title="对比表格" style="margin-top: 20px">
|
<n-card v-if="comparisons.length > 0" title="对比表格" style="margin-top: 20px">
|
||||||
<n-data-table
|
<n-data-table
|
||||||
|
|||||||
Reference in New Issue
Block a user