From f581029eb21b76fab7a1a9bac0984a15595f9098 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Thu, 10 Sep 2026 19:46:32 -0600 Subject: [PATCH] =?UTF-8?q?refactor(=E7=BB=9F=E8=AE=A1=E9=9D=A2=E6=9D=BF):?= =?UTF-8?q?=20=E6=8A=BD=E5=87=BA=20useHiddenStudents=EF=BC=8C=E4=B8=A4?= =?UTF-8?q?=E4=BB=BD=E9=80=90=E5=AD=97=E7=9B=B8=E5=90=8C=E7=9A=84=E3=80=8C?= =?UTF-8?q?=E8=AF=B7=E5=81=87=E9=9A=90=E8=97=8F=E3=80=8D=E5=90=88=E6=88=90?= =?UTF-8?q?=E4=B8=80=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH --- .../components/FlowchartStatisticsPanel.vue | 60 ++------------- .../src/shared/components/StatisticsPanel.vue | 47 ++---------- .../src/shared/composables/hiddenStudents.ts | 76 +++++++++++++++++++ 3 files changed, 88 insertions(+), 95 deletions(-) create mode 100644 apps/web/src/shared/composables/hiddenStudents.ts diff --git a/apps/web/src/shared/components/FlowchartStatisticsPanel.vue b/apps/web/src/shared/components/FlowchartStatisticsPanel.vue index 9a1c789..719eaec 100644 --- a/apps/web/src/shared/components/FlowchartStatisticsPanel.vue +++ b/apps/web/src/shared/components/FlowchartStatisticsPanel.vue @@ -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("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 { - try { - return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}") - } catch { - return {} - } -} +const visibleUnaccepted = computed(() => data.dataUnaccepted.filter(notHidden)) -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.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( diff --git a/apps/web/src/shared/components/StatisticsPanel.vue b/apps/web/src/shared/components/StatisticsPanel.vue index a04e4bd..5b2e024 100644 --- a/apps/web/src/shared/components/StatisticsPanel.vue +++ b/apps/web/src/shared/components/StatisticsPanel.vue @@ -201,6 +201,7 @@ import { getSubmissionStatistics, getSubmissionStatisticsItems } from "oj/api" import { DURATION_OPTIONS, STORAGE_KEY } from "utils/constants" import storage from "utils/storage" import { useConfigStore } from "../store/config" +import { useHiddenStudents } from "../composables/hiddenStudents" import { Doughnut } from "vue-chartjs" import { Chart as ChartJS, ArcElement, Title, Tooltip, Legend } from "chart.js" import { NButton, NFlex, NTag, NText, type DataTableRowKey } from "naive-ui" @@ -400,41 +401,8 @@ const hasResult = computed( () => count.total > 0 || listUnaccepted.value.length > 0, ) -const HIDE_DURATION = 2 * 60 * 60 * 1000 -const HIDDEN_KEY = "oj_hidden_students" - -function loadHidden(): Record { - try { - return JSON.parse(localStorage.getItem(HIDDEN_KEY) ?? "{}") - } catch { - return {} - } -} - -const hiddenStudents = ref>(loadHidden()) -const hideMode = ref(false) - -function saveHidden(data: Record) { - localStorage.setItem(HIDDEN_KEY, JSON.stringify(data)) -} - -function hideStudent(username: string) { - hiddenStudents.value = { - ...hiddenStudents.value, - [username]: Date.now() + HIDE_DURATION, - } - saveHidden(hiddenStudents.value) -} - -function showAll() { - hiddenStudents.value = {} - saveHidden({}) -} - -function notHidden(item: { username: string }) { - const exp = hiddenStudents.value[item.username] - return !exp || exp <= Date.now() -} +const { hideMode, hideStudent, showAll, notHidden } = + useHiddenStudents("oj_hidden_students") const visibleUnaccepted = computed(() => listUnaccepted.value.filter(notHidden)) const visibleAttempted = computed(() => listAttempted.value.filter(notHidden)) @@ -524,13 +492,8 @@ const adjustedPersonRate = computed(() => { }) onMounted(() => { - const now = Date.now() - const cleaned = Object.fromEntries( - Object.entries(hiddenStudents.value).filter(([, exp]) => exp > now), - ) - hiddenStudents.value = cleaned - saveHidden(cleaned) - // 打开就查一次。老师是投在屏幕上盯着看的,不该还要先点一下按钮 + // 过期清理在 useHiddenStudents 里,这里只管「打开就查一次」—— + // 老师是投在屏幕上盯着看的,不该还要先点一下按钮 handleStatistics() }) diff --git a/apps/web/src/shared/composables/hiddenStudents.ts b/apps/web/src/shared/composables/hiddenStudents.ts new file mode 100644 index 0000000..2101180 --- /dev/null +++ b/apps/web/src/shared/composables/hiddenStudents.ts @@ -0,0 +1,76 @@ +import { onMounted, ref } from "vue" + +/** + * 统计面板的「暂时隐藏某个学生」。 + * + * 老师是把面板投在屏幕上盯着看的,未完成名单里总有那么几个是请假/转班/学号错了的, + * 一直挂在上面会盖住真正需要盯的人。隐藏是**带过期时间**的(两小时,够一节课), + * 不是永久删除 —— 下节课自动回来,免得有人被无声地漏掉。 + * + * 存在 localStorage 而不是后端:这是「这台电脑上这位老师这节课不想看谁」, + * 换个人、换台机器都不该继承。 + * + * 原来这套(loadHidden / saveHidden / hideStudent / showAll / 过期清理)在 + * StatisticsPanel.vue 和 FlowchartStatisticsPanel.vue 里各写了一遍,除了存储键 + * 和一个参数名逐字相同;判断「有没有被隐藏」两边还各自内联了三处。 + * + * @param storageKey localStorage 的键。**两个面板各用各的** —— 提交统计里隐掉的人 + * 不该连带在流程图统计里也消失,那是两件事。 + */ +export function useHiddenStudents(storageKey: string) { + /** 隐藏时长:两小时,一节课的量级 */ + const HIDE_DURATION = 2 * 60 * 60 * 1000 + + function load(): Record { + try { + return JSON.parse(localStorage.getItem(storageKey) ?? "{}") + } catch { + return {} + } + } + + const hiddenStudents = ref>(load()) + /** 面板上的「隐藏模式」开关:打开后每行才出现那个隐藏按钮 */ + const hideMode = ref(false) + + function save(data: Record) { + localStorage.setItem(storageKey, JSON.stringify(data)) + } + + function hideStudent(username: string) { + hiddenStudents.value = { + ...hiddenStudents.value, + [username]: Date.now() + HIDE_DURATION, + } + save(hiddenStudents.value) + } + + function showAll() { + hiddenStudents.value = {} + save({}) + } + + function isHidden(username: string) { + const expiresAt = hiddenStudents.value[username] + return !!expiresAt && expiresAt > Date.now() + } + + /** 给 filter 用:`list.filter(notHidden)` */ + function notHidden(item: { username: string }) { + return !isHidden(item.username) + } + + onMounted(() => { + // 把已经到期的清掉再落一次盘,否则这张表只增不减 + const now = Date.now() + const cleaned = Object.fromEntries( + Object.entries(hiddenStudents.value).filter(([, expiresAt]) => expiresAt > now), + ) + hiddenStudents.value = cleaned + save(cleaned) + }) + + // 不导出 hiddenStudents 本身:两个面板要的都是「这个人该不该显示」, + // 把那张表递出去只会让判断逻辑又散回各自的组件里 + return { hideMode, hideStudent, showAll, isHidden, notHidden } +}