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:
@@ -159,6 +159,7 @@ import { formatISO, sub, type Duration } from "date-fns"
|
|||||||
import type { FlowchartStatistics } from "@oj2/contract"
|
import type { FlowchartStatistics } from "@oj2/contract"
|
||||||
import { getFlowchartStatistics } from "oj/api"
|
import { getFlowchartStatistics } from "oj/api"
|
||||||
import { DURATION_OPTIONS, FLOWCHART_CRITERIA_ORDER } from "utils/constants"
|
import { DURATION_OPTIONS, FLOWCHART_CRITERIA_ORDER } from "utils/constants"
|
||||||
|
import { useHiddenStudents } from "../composables/hiddenStudents"
|
||||||
import { Doughnut, Radar, Bar } from "vue-chartjs"
|
import { Doughnut, Radar, Bar } from "vue-chartjs"
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
@@ -237,66 +238,19 @@ const hasResult = computed(
|
|||||||
const wordcloudCanvas = useTemplateRef<HTMLCanvasElement>("wordcloudCanvas")
|
const wordcloudCanvas = useTemplateRef<HTMLCanvasElement>("wordcloudCanvas")
|
||||||
let wordcloudChart: ChartJS | null = null
|
let wordcloudChart: ChartJS | null = null
|
||||||
|
|
||||||
const HIDE_DURATION = 2 * 60 * 60 * 1000
|
const { hideMode, hideStudent, showAll, isHidden, notHidden } =
|
||||||
const STORAGE_KEY = "oj_hidden_students_flowchart"
|
useHiddenStudents("oj_hidden_students_flowchart")
|
||||||
|
|
||||||
function loadHidden(): Record<string, number> {
|
const visibleUnaccepted = computed(() => data.dataUnaccepted.filter(notHidden))
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}")
|
|
||||||
} catch {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hiddenStudents = ref<Record<string, number>>(loadHidden())
|
const hiddenCount = computed(
|
||||||
const hideMode = ref(false)
|
() => data.dataUnaccepted.filter((item) => isHidden(item.username)).length,
|
||||||
|
)
|
||||||
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 adjustedPersonCount = computed(() =>
|
const adjustedPersonCount = computed(() =>
|
||||||
Math.max(0, data.personCount - hiddenCount.value),
|
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(() => {
|
const completionRate = computed(() => {
|
||||||
if (adjustedPersonCount.value <= 0) return "0%"
|
if (adjustedPersonCount.value <= 0) return "0%"
|
||||||
const rate = Math.min(
|
const rate = Math.min(
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ import { getSubmissionStatistics, getSubmissionStatisticsItems } from "oj/api"
|
|||||||
import { DURATION_OPTIONS, STORAGE_KEY } from "utils/constants"
|
import { DURATION_OPTIONS, STORAGE_KEY } from "utils/constants"
|
||||||
import storage from "utils/storage"
|
import storage from "utils/storage"
|
||||||
import { useConfigStore } from "../store/config"
|
import { useConfigStore } from "../store/config"
|
||||||
|
import { useHiddenStudents } from "../composables/hiddenStudents"
|
||||||
import { Doughnut } from "vue-chartjs"
|
import { Doughnut } from "vue-chartjs"
|
||||||
import { Chart as ChartJS, ArcElement, Title, Tooltip, Legend } from "chart.js"
|
import { Chart as ChartJS, ArcElement, Title, Tooltip, Legend } from "chart.js"
|
||||||
import { NButton, NFlex, NTag, NText, type DataTableRowKey } from "naive-ui"
|
import { NButton, NFlex, NTag, NText, type DataTableRowKey } from "naive-ui"
|
||||||
@@ -400,41 +401,8 @@ const hasResult = computed(
|
|||||||
() => count.total > 0 || listUnaccepted.value.length > 0,
|
() => count.total > 0 || listUnaccepted.value.length > 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
const HIDE_DURATION = 2 * 60 * 60 * 1000
|
const { hideMode, hideStudent, showAll, notHidden } =
|
||||||
const HIDDEN_KEY = "oj_hidden_students"
|
useHiddenStudents("oj_hidden_students")
|
||||||
|
|
||||||
function loadHidden(): Record<string, number> {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem(HIDDEN_KEY) ?? "{}")
|
|
||||||
} catch {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hiddenStudents = ref<Record<string, number>>(loadHidden())
|
|
||||||
const hideMode = ref(false)
|
|
||||||
|
|
||||||
function saveHidden(data: Record<string, number>) {
|
|
||||||
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 visibleUnaccepted = computed(() => listUnaccepted.value.filter(notHidden))
|
const visibleUnaccepted = computed(() => listUnaccepted.value.filter(notHidden))
|
||||||
const visibleAttempted = computed(() => listAttempted.value.filter(notHidden))
|
const visibleAttempted = computed(() => listAttempted.value.filter(notHidden))
|
||||||
@@ -524,13 +492,8 @@ const adjustedPersonRate = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const now = Date.now()
|
// 过期清理在 useHiddenStudents 里,这里只管「打开就查一次」——
|
||||||
const cleaned = Object.fromEntries(
|
// 老师是投在屏幕上盯着看的,不该还要先点一下按钮
|
||||||
Object.entries(hiddenStudents.value).filter(([, exp]) => exp > now),
|
|
||||||
)
|
|
||||||
hiddenStudents.value = cleaned
|
|
||||||
saveHidden(cleaned)
|
|
||||||
// 打开就查一次。老师是投在屏幕上盯着看的,不该还要先点一下按钮
|
|
||||||
handleStatistics()
|
handleStatistics()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
76
apps/web/src/shared/composables/hiddenStudents.ts
Normal file
76
apps/web/src/shared/composables/hiddenStudents.ts
Normal file
@@ -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<string, number> {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(storageKey) ?? "{}")
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenStudents = ref<Record<string, number>>(load())
|
||||||
|
/** 面板上的「隐藏模式」开关:打开后每行才出现那个隐藏按钮 */
|
||||||
|
const hideMode = ref(false)
|
||||||
|
|
||||||
|
function save(data: Record<string, number>) {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user