From e33ef710af8a2cfbeb3679c84daf61c8f6e25519 Mon Sep 17 00:00:00 2001
From: yuetsh <517252939@qq.com>
Date: Wed, 3 Jun 2026 08:54:10 -0600
Subject: [PATCH] add a chart
---
.../components/FlowchartStatisticsPanel.vue | 154 ++++++++++++++++--
1 file changed, 142 insertions(+), 12 deletions(-)
diff --git a/src/shared/components/FlowchartStatisticsPanel.vue b/src/shared/components/FlowchartStatisticsPanel.vue
index 2a15d54..2d59daa 100644
--- a/src/shared/components/FlowchartStatisticsPanel.vue
+++ b/src/shared/components/FlowchartStatisticsPanel.vue
@@ -88,6 +88,12 @@
+
+
+
+
+
+
@@ -102,16 +108,42 @@
-
-
+
+ 请假隐藏中
+ 请假隐藏
+
+
- {{ item.real_name }}
-
+ 恢复 {{ hiddenCount }} 位
+
+
+
+
+ 全都完成了
+
+
+
+ {{ item.real_name }}
+
+ {{ item.real_name }}
+
@@ -122,7 +154,7 @@
import { formatISO, sub, type Duration } from "date-fns"
import { getFlowchartStatistics } from "oj/api"
import { DURATION_OPTIONS } from "utils/constants"
-import { Doughnut, Radar } from "vue-chartjs"
+import { Doughnut, Radar, Bar } from "vue-chartjs"
import {
Chart as ChartJS,
ArcElement,
@@ -134,6 +166,8 @@ import {
LineElement,
Filler,
LinearScale,
+ BarElement,
+ CategoryScale,
} from "chart.js"
import {
WordCloudController,
@@ -150,6 +184,8 @@ ChartJS.register(
LineElement,
Filler,
LinearScale,
+ BarElement,
+ CategoryScale,
WordCloudController,
WordElement,
)
@@ -200,11 +236,71 @@ const data = reactive({
const wordcloudCanvas = useTemplateRef("wordcloudCanvas")
let wordcloudChart: ChartJS | null = null
+const HIDE_DURATION = 2 * 60 * 60 * 1000
+const STORAGE_KEY = "oj_hidden_students_flowchart"
+
+function loadHidden(): Record {
+ try {
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}")
+ } catch {
+ return {}
+ }
+}
+
+const hiddenStudents = ref>(loadHidden())
+const hideMode = ref(false)
+
+function saveHidden(d: Record) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(d))
+}
+
+function hideStudent(username: string) {
+ hiddenStudents.value = {
+ ...hiddenStudents.value,
+ [username]: Date.now() + HIDE_DURATION,
+ }
+ saveHidden(hiddenStudents.value)
+}
+
+function showAll() {
+ hiddenStudents.value = {}
+ saveHidden({})
+}
+
+const visibleUnaccepted = computed(() => {
+ const now = Date.now()
+ return data.data_unaccepted.filter((item) => {
+ const exp = hiddenStudents.value[item.username]
+ return !exp || exp <= now
+ })
+})
+
+const hiddenCount = computed(() => {
+ const now = Date.now()
+ return data.data_unaccepted.filter((item) => {
+ const exp = hiddenStudents.value[item.username]
+ return !!exp && exp > now
+ }).length
+})
+
+const adjustedPersonCount = computed(() =>
+ Math.max(0, data.person_count - hiddenCount.value),
+)
+
+onMounted(() => {
+ const now = Date.now()
+ const cleaned = Object.fromEntries(
+ Object.entries(hiddenStudents.value).filter(([, exp]) => exp > now),
+ )
+ hiddenStudents.value = cleaned
+ saveHidden(cleaned)
+})
+
const completionRate = computed(() => {
- if (data.person_count <= 0) return "0%"
+ if (adjustedPersonCount.value <= 0) return "0%"
const rate = Math.min(
100,
- (data.completed_count / data.person_count) * 100,
+ (data.completed_count / adjustedPersonCount.value) * 100,
)
return `${Math.round(rate * 100) / 100}%`
})
@@ -236,7 +332,7 @@ const gradeChartData = computed(() => {
})
const completionChartData = computed(() => {
- const uncompleted = Math.max(0, data.person_count - data.completed_count)
+ const uncompleted = Math.max(0, adjustedPersonCount.value - data.completed_count)
return {
labels: ["已完成", "未完成"],
datasets: [
@@ -328,6 +424,40 @@ const radarOptions = {
},
}
+const criteriaBarChartData = computed(() => {
+ const labels = CRITERIA_ORDER.filter((k) => k in data.criteria_averages)
+ return {
+ labels,
+ datasets: [
+ {
+ label: "平均得分",
+ data: labels.map((k) => data.criteria_averages[k]?.avg ?? 0),
+ backgroundColor: "rgba(32, 128, 240, 0.6)",
+ borderColor: "rgba(32, 128, 240, 1)",
+ borderWidth: 2,
+ },
+ {
+ label: "满分",
+ data: labels.map((k) => data.criteria_averages[k]?.max ?? 0),
+ backgroundColor: "rgba(200, 200, 200, 0.3)",
+ borderColor: "rgba(150, 150, 150, 0.8)",
+ borderWidth: 1,
+ },
+ ],
+ }
+})
+
+const barOptions = {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: { beginAtZero: true },
+ },
+ plugins: {
+ legend: { position: "bottom" as const },
+ },
+}
+
const WORD_COLORS = [
"#2080f0",
"#18a058",