refactor(统计面板): 抽出 useHiddenStudents,两份逐字相同的「请假隐藏」合成一份

StatisticsPanel.vue 和 FlowchartStatisticsPanel.vue 各写了一遍隐藏学生的逻辑
(loadHidden / saveHidden / hideStudent / showAll / onMounted 里的过期清理),除了
存储键和一个参数名逐字相同;「这个人有没有被隐藏」的判断两边还各自内联了三处。

现在是 shared/composables/hiddenStudents.ts,存储键作参数传进去 —— 两个面板仍然各用
各的键,提交统计里隐掉的人不该连带在流程图统计里也消失,那是两件事。

composable 不导出 hiddenStudents 那张表本身,只给 isHidden / notHidden:两个面板要的
都是「这个人该不该显示」,把表递出去只会让判断逻辑又散回组件里。

## 验证

起全栈在浏览器里真点了一遍(提交列表 → 数据统计 → 未完成 tab):

- 打开「请假隐藏」开关后学生标签变成可关闭,关掉「单田芳」,未完成从 5 变 4、
  「还没交」也同步从 5 变 4;
- localStorage 落的是 {"ks251单田芳": <时间戳>},刷新页面后仍在,到期时间 120 分钟;
- 流程图那把键 oj_hidden_students_flowchart 全程为 null,没被串到。

vue-tsc、vite build 通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
This commit is contained in:
xuyueandClaude Opus 5 committed 2026-09-10 19:46:32 -06:00
1 parent eda1eb7eee
commit f581029eb2
3 files changed
+88 -95

No files matched your search

@@ -159,6 +159,7 @@ import { formatISO, sub, type Duration } from "date-fns"
import type { FlowchartStatistics } from "@oj2/contract"
import { getFlowchartStatistics } from "oj/api"
import { DURATION_OPTIONS, FLOWCHART_CRITERIA_ORDER } from "utils/constants"
import { useHiddenStudents } from "../composables/hiddenStudents"
import { Doughnut, Radar, Bar } from "vue-chartjs"
import {
Chart as ChartJS,
@@ -237,66 +238,19 @@ const hasResult = computed(
const wordcloudCanvas = useTemplateRef<HTMLCanvasElement>("wordcloudCanvas")
let wordcloudChart: ChartJS | null = null
const HIDE_DURATION = 2 * 60 * 60 * 1000
const STORAGE_KEY = "oj_hidden_students_flowchart"
const { hideMode, hideStudent, showAll, isHidden, notHidden } =
useHiddenStudents("oj_hidden_students_flowchart")
function loadHidden(): Record<string, number> {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}")
} catch {
return {}
}
}
const visibleUnaccepted = computed(() => data.dataUnaccepted.filter(notHidden))
const hiddenStudents = ref<Record<string, number>>(loadHidden())
const hideMode = ref(false)
function saveHidden(d: Record<string, number>) {
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.dataUnaccepted.filter((item) => {
const exp = hiddenStudents.value[item.username]
return !exp || exp <= now
})
})
const hiddenCount = computed(() => {
const now = Date.now()
return data.dataUnaccepted.filter((item) => {
const exp = hiddenStudents.value[item.username]
return !!exp && exp > now
}).length
})
const hiddenCount = computed(
() => data.dataUnaccepted.filter((item) => isHidden(item.username)).length,
)
const adjustedPersonCount = computed(() =>
Math.max(0, data.personCount - 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 (adjustedPersonCount.value <= 0) return "0%"
const rate = Math.min(