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 } +}