原来要先打开开关、名字变成带 × 的标签再点 ×,× 像删除;标完人从页面上消失, 恢复只能全部恢复。现在: - 班级下拉旁「请假」按钮,标了人之后按钮直接写「请假 2 人:陈言、黄依露 · 修改」 - 弹框只列今天还没交过的人(请假的人一定在里面,刚上课时就是全班),勾上点确定 - 请假的人在表格里变灰排到最后,不算进进度和「还没交过」 - 交了题或举手就算来了,自动取消标记 - 有效期从两小时改成今天(请假一般是一整天),仍然只记在这台电脑上 顺带修掉:被标过的学生后来交了题,会从表格里整个消失、也不算进进度。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af2ca31de5
commit
1ba8928d41
4 files changed
+262
-129
No files matched your search
+1
-1
@@ -59,7 +59,7 @@ API 调用不按模块分:学生端全在 `oj/api.ts`、后台全在 `admin/ap
|
|||||||
|
|
||||||
Shared logic lives in `shared/`:
|
Shared logic lives in `shared/`:
|
||||||
- `store/` — Pinia stores: `user` (auth/roles), `config` (site-wide settings), `authModal` (login/signup form state), `collab` (help-request queue + collab room), `achievement` (解锁弹窗队列), `myFlowchart` (题面「你画的流程图」那张 A/S 图的 mermaid 源码)
|
- `store/` — Pinia stores: `user` (auth/roles), `config` (site-wide settings), `authModal` (login/signup form state), `collab` (help-request queue + collab room), `achievement` (解锁弹窗队列), `myFlowchart` (题面「你画的流程图」那张 A/S 图的 mermaid 源码)
|
||||||
- `composables/` — `aiStream`, `pagination` (URL-synced), `websocket` (reconnect + heartbeat), `collabDoc` (Yjs binding for the collab channel), `configUpdate` (WS-pushed config sync), `useMermaid` (lazy Mermaid render), `darkTransition` (View Transitions,111 以下走降级分支), `hiddenStudents` (课堂看板的「请假隐藏」), `chartTheme`, `breakpoints`, `maxkb`, `learnProgress`, `rarity`
|
- `composables/` — `aiStream`, `pagination` (URL-synced), `websocket` (reconnect + heartbeat), `collabDoc` (Yjs binding for the collab channel), `configUpdate` (WS-pushed config sync), `useMermaid` (lazy Mermaid render), `darkTransition` (View Transitions,111 以下走降级分支), `leaveStudents` (课堂看板的「请假」), `chartTheme`, `breakpoints`, `maxkb`, `learnProgress`, `rarity`
|
||||||
- `layout/` — `default.vue` and `admin.vue` layout wrappers
|
- `layout/` — `default.vue` and `admin.vue` layout wrappers
|
||||||
- `api.ts` — shared API calls (auth, profile, tags, captcha)
|
- `api.ts` — shared API calls (auth, profile, tags, captcha)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getClassBoard, setClassLesson } from "oj/api"
|
import { getClassBoard, setClassLesson } from "oj/api"
|
||||||
import { useHiddenStudents } from "shared/composables/hiddenStudents"
|
import { useLeaveStudents } from "shared/composables/leaveStudents"
|
||||||
import { useCollabStore } from "shared/store/collab"
|
import { useCollabStore } from "shared/store/collab"
|
||||||
import { useConfigStore } from "shared/store/config"
|
import { useConfigStore } from "shared/store/config"
|
||||||
import type { RunnableLanguage } from "@oj2/contract"
|
import type { RunnableLanguage } from "@oj2/contract"
|
||||||
@@ -119,7 +119,7 @@ onUnmounted(pause)
|
|||||||
|
|
||||||
// ---------- 分组与排序 ----------
|
// ---------- 分组与排序 ----------
|
||||||
|
|
||||||
type Group = "help" | "idle" | "stuck" | "working" | "done"
|
type Group = "help" | "idle" | "stuck" | "working" | "done" | "leave"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 正在课堂求助的学生。列表是老师端 WebSocket 实时推的(collabStore,全局常驻连接),
|
* 正在课堂求助的学生。列表是老师端 WebSocket 实时推的(collabStore,全局常驻连接),
|
||||||
@@ -140,6 +140,7 @@ function oftenIdle(student: ClassBoardStudent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function groupOf(student: ClassBoardStudent): Group {
|
function groupOf(student: ClassBoardStudent): Group {
|
||||||
|
if (absent(student)) return "leave"
|
||||||
if (helpOf(student)) return "help"
|
if (helpOf(student)) return "help"
|
||||||
const cells = student.cells
|
const cells = student.cells
|
||||||
if (cells.length && cells.every((cell) => cell.status === "accepted")) return "done"
|
if (cells.length && cells.every((cell) => cell.status === "accepted")) return "done"
|
||||||
@@ -149,41 +150,81 @@ function groupOf(student: ClassBoardStudent): Group {
|
|||||||
return "working"
|
return "working"
|
||||||
}
|
}
|
||||||
|
|
||||||
const GROUP_ORDER: Record<Group, number> = { help: 0, idle: 1, stuck: 2, working: 3, done: 4 }
|
const GROUP_ORDER: Record<Group, number> = {
|
||||||
|
help: 0,
|
||||||
|
idle: 1,
|
||||||
|
stuck: 2,
|
||||||
|
working: 3,
|
||||||
|
done: 4,
|
||||||
|
leave: 5,
|
||||||
|
}
|
||||||
|
|
||||||
function nameOf(student: ClassBoardStudent) {
|
function nameOf(student: ClassBoardStudent) {
|
||||||
return student.realName || student.username
|
return student.realName || student.username
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请假隐藏(原来在「数据统计」弹框里,统计改成回头看之后挪到这儿 —— 盯这节课的是看板)。
|
* 请假:老师在「请假」框里勾上今天不在的人,看板的人数、进度、「还没交过」都不算他们,
|
||||||
* 请假、转班、学号错了的那几个一直挂在「还没交过」里,会盖住真正要去看的人。
|
* 表格里变灰排到最后(不藏掉 —— 标了谁、标错没有,一眼看得见)。
|
||||||
* 藏两个小时(够一节课),下节课自动回来;只记在这台电脑上,换人换机器都不继承。
|
* 今天有效,只记在这台电脑上(见 leaveStudents.ts)。
|
||||||
* 键沿用统计弹框那份,老师上午藏的人这里接着藏着。
|
|
||||||
*
|
*
|
||||||
* **只藏「还没交过」的人**:一个学生只要今天交过、或者正在举手,就不算请假,照常显示、
|
* **只有今天还没交过的人能算请假**:请假的人一定在这里面。标过的人后来交了题、或者举手了,
|
||||||
* 照常算进人数。迟到的学生后来交了题、卡住了、举手了,老师得看得见
|
* 就是来了(迟到),自动取消标记,回到正常的分组里 —— 老师得看得见他卡住、举手
|
||||||
*/
|
*/
|
||||||
const { hideMode, hideStudent, unhide, isHidden } = useHiddenStudents("oj_hidden_students")
|
const { onLeave, setLeave, cancelLeave } = useLeaveStudents("oj_leave_students")
|
||||||
function hiddenNow(student: ClassBoardStudent) {
|
function canLeave(student: ClassBoardStudent) {
|
||||||
return !student.lastSubmitAt && !helpOf(student) && isHidden(student.username)
|
return !student.lastSubmitAt && !helpOf(student)
|
||||||
}
|
}
|
||||||
const hiddenStudents = computed(() => (board.value?.students ?? []).filter(hiddenNow))
|
function absent(student: ClassBoardStudent) {
|
||||||
function restoreHidden() {
|
return canLeave(student) && onLeave(student.username)
|
||||||
unhide(hiddenStudents.value.map((student) => student.username))
|
}
|
||||||
|
const boardStudents = computed(() => board.value?.students ?? [])
|
||||||
|
const leaveStudents = computed(() => boardStudents.value.filter(absent).sort(byName))
|
||||||
|
const leaveNames = computed(() => leaveStudents.value.map(nameOf).join("、"))
|
||||||
|
|
||||||
|
// 标过请假、后来交了题或举手的:来了,把标记清掉(不然名单里一直挂着一个其实在的人)
|
||||||
|
watchEffect(() => {
|
||||||
|
const arrived = boardStudents.value
|
||||||
|
.filter((student) => !canLeave(student) && onLeave(student.username))
|
||||||
|
.map((student) => student.username)
|
||||||
|
if (arrived.length) cancelLeave(arrived)
|
||||||
|
})
|
||||||
|
|
||||||
|
const leaveDialog = ref(false)
|
||||||
|
/** 弹框里能勾的:今天还没交过的(刚上课时就是全班),按名字排 */
|
||||||
|
const leaveCandidates = computed(() => boardStudents.value.filter(canLeave).sort(byName))
|
||||||
|
const leaveSelected = ref<string[]>([])
|
||||||
|
function openLeaveDialog() {
|
||||||
|
leaveSelected.value = leaveStudents.value.map((student) => student.username)
|
||||||
|
leaveDialog.value = true
|
||||||
|
}
|
||||||
|
function saveLeave() {
|
||||||
|
setLeave(
|
||||||
|
boardStudents.value.map((student) => student.username),
|
||||||
|
leaveSelected.value,
|
||||||
|
)
|
||||||
|
leaveDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function byName(a: ClassBoardStudent, b: ClassBoardStudent) {
|
||||||
|
return nameOf(a).localeCompare(nameOf(b), "zh-CN")
|
||||||
}
|
}
|
||||||
|
|
||||||
const students = computed(() =>
|
const students = computed(() =>
|
||||||
[...(board.value?.students ?? [])]
|
[...boardStudents.value].sort(
|
||||||
.filter((student) => !isHidden(student.username))
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
GROUP_ORDER[groupOf(a)] - GROUP_ORDER[groupOf(b)] ||
|
GROUP_ORDER[groupOf(a)] - GROUP_ORDER[groupOf(b)] ||
|
||||||
// 同样是没交过,经常不动手的排前面
|
// 同样是没交过,经常不动手的排前面
|
||||||
Number(oftenIdle(b)) - Number(oftenIdle(a)) ||
|
Number(oftenIdle(b)) - Number(oftenIdle(a)) ||
|
||||||
nameOf(a).localeCompare(nameOf(b), "zh-CN"),
|
byName(a, b),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
/** 到了的人:人数、进度都按这些算 */
|
||||||
|
const present = computed(() => students.value.filter((student) => groupOf(student) !== "leave"))
|
||||||
|
/** 表格里第一个请假的人,在他上面插一行说明 */
|
||||||
|
const firstLeaveId = computed(
|
||||||
|
() => students.value.find((student) => groupOf(student) === "leave")?.userId,
|
||||||
|
)
|
||||||
|
|
||||||
const idle = computed(() => students.value.filter((student) => groupOf(student) === "idle"))
|
const idle = computed(() => students.value.filter((student) => groupOf(student) === "idle"))
|
||||||
const idleOften = computed(() => idle.value.filter(oftenIdle))
|
const idleOften = computed(() => idle.value.filter(oftenIdle))
|
||||||
@@ -203,7 +244,7 @@ const stuckCount = computed(
|
|||||||
|
|
||||||
const summary = computed(() => {
|
const summary = computed(() => {
|
||||||
// 分母也减掉请假的,不然「做完 30/38」永远到不了头
|
// 分母也减掉请假的,不然「做完 30/38」永远到不了头
|
||||||
const all = students.value
|
const all = present.value
|
||||||
return (board.value?.problems ?? []).map((problem, i) => ({
|
return (board.value?.problems ?? []).map((problem, i) => ({
|
||||||
...problem,
|
...problem,
|
||||||
done: all.filter((student) => student.cells[i]?.status === "accepted").length,
|
done: all.filter((student) => student.cells[i]?.status === "accepted").length,
|
||||||
@@ -236,6 +277,7 @@ const GROUP_LABEL: Record<
|
|||||||
stuck: { text: "卡住了", type: "error" },
|
stuck: { text: "卡住了", type: "error" },
|
||||||
working: { text: "在做", type: "info" },
|
working: { text: "在做", type: "info" },
|
||||||
done: { text: "做完了", type: "success" },
|
done: { text: "做完了", type: "success" },
|
||||||
|
leave: { text: "请假", type: "default" },
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -283,6 +325,20 @@ function submissionsHref(
|
|||||||
style="width: 180px"
|
style="width: 180px"
|
||||||
@update:value="changeClass"
|
@update:value="changeClass"
|
||||||
/>
|
/>
|
||||||
|
<!-- 投影时不出名字,请假这个按钮也收起来 -->
|
||||||
|
<n-button
|
||||||
|
v-if="board?.className && !projector"
|
||||||
|
:type="leaveStudents.length ? 'warning' : 'default'"
|
||||||
|
:secondary="!!leaveStudents.length"
|
||||||
|
class="leave-button"
|
||||||
|
@click="openLeaveDialog"
|
||||||
|
>
|
||||||
|
<template v-if="leaveStudents.length">
|
||||||
|
<span class="leave-names">请假 {{ leaveStudents.length }} 人:{{ leaveNames }}</span>
|
||||||
|
<span class="leave-edit">修改</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>请假</template>
|
||||||
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<n-text depth="3" class="meta">
|
<n-text depth="3" class="meta">
|
||||||
@@ -378,38 +434,14 @@ function submissionsHref(
|
|||||||
|
|
||||||
<template v-if="!projector">
|
<template v-if="!projector">
|
||||||
<n-alert
|
<n-alert
|
||||||
v-if="idle.length || hiddenStudents.length"
|
v-if="idle.length"
|
||||||
type="warning"
|
type="warning"
|
||||||
:title="idle.length ? `今天还没交过(${idle.length} 人)` : '今天没交过的都隐藏了'"
|
:title="`今天还没交过(${idle.length} 人)`"
|
||||||
class="section"
|
class="section"
|
||||||
>
|
>
|
||||||
<n-flex align="center" :size="10" class="hide-bar">
|
<n-text v-if="leaveStudents.length" depth="3" class="meta">
|
||||||
<n-switch v-model:value="hideMode" size="small">
|
请假的 {{ leaveStudents.length }} 人不算在里面
|
||||||
<template #checked>请假隐藏中 · 点名字旁的 × 藏起来</template>
|
</n-text>
|
||||||
<template #unchecked>请假隐藏</template>
|
|
||||||
</n-switch>
|
|
||||||
<n-button
|
|
||||||
v-if="hiddenStudents.length"
|
|
||||||
size="tiny"
|
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
@click="restoreHidden"
|
|
||||||
>
|
|
||||||
隐藏了 {{ hiddenStudents.length }} 位:{{ hiddenStudents.map(nameOf).join("、") }} ·
|
|
||||||
恢复
|
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
|
||||||
<n-flex v-if="hideMode" :size="8">
|
|
||||||
<n-tag
|
|
||||||
v-for="student in idle"
|
|
||||||
:key="student.userId"
|
|
||||||
closable
|
|
||||||
@close="hideStudent(student.username)"
|
|
||||||
>
|
|
||||||
{{ nameOf(student) }}
|
|
||||||
</n-tag>
|
|
||||||
</n-flex>
|
|
||||||
<template v-else>
|
|
||||||
<div v-if="idleOften.length">
|
<div v-if="idleOften.length">
|
||||||
<b>经常没交</b>(最近 {{ board.recentLessons }} 节课最多交过 1 节):{{
|
<b>经常没交</b>(最近 {{ board.recentLessons }} 节课最多交过 1 节):{{
|
||||||
idleOften.map(nameOf).join("、")
|
idleOften.map(nameOf).join("、")
|
||||||
@@ -419,7 +451,6 @@ function submissionsHref(
|
|||||||
<template v-if="idleOften.length"><b>其他</b>:</template
|
<template v-if="idleOften.length"><b>其他</b>:</template
|
||||||
>{{ idleOthers.map(nameOf).join("、") }}
|
>{{ idleOthers.map(nameOf).join("、") }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
|
||||||
<n-text v-if="stuckCount" type="error" class="meta">
|
<n-text v-if="stuckCount" type="error" class="meta">
|
||||||
@@ -443,7 +474,14 @@ function submissionsHref(
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="student in students" :key="student.userId">
|
<template v-for="student in students" :key="student.userId">
|
||||||
|
<tr v-if="student.userId === firstLeaveId" class="leave-sep">
|
||||||
|
<td :colspan="board.problems.length + 4">
|
||||||
|
请假 {{ leaveStudents.length }} 人 · 不算进上面的人数和进度 ·
|
||||||
|
交了题或举手就自动回到上面
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr :class="{ 'leave-row': groupOf(student) === 'leave' }">
|
||||||
<td class="name">
|
<td class="name">
|
||||||
<a
|
<a
|
||||||
v-if="student.codeToday || student.drawnToday"
|
v-if="student.codeToday || student.drawnToday"
|
||||||
@@ -498,18 +536,70 @@ function submissionsHref(
|
|||||||
<td class="meta">
|
<td class="meta">
|
||||||
{{ student.lastSubmitAt ? parseTime(student.lastSubmitAt, "HH:mm") : "—" }}
|
{{ student.lastSubmitAt ? parseTime(student.lastSubmitAt, "HH:mm") : "—" }}
|
||||||
</td>
|
</td>
|
||||||
<td :class="{ 'cell-stuck': oftenIdle(student) }">
|
<td
|
||||||
|
:class="{
|
||||||
|
'cell-stuck': oftenIdle(student) && groupOf(student) !== 'leave',
|
||||||
|
}"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
board.recentLessons ? `${student.recentAttended}/${board.recentLessons}` : "—"
|
board.recentLessons
|
||||||
|
? `${student.recentAttended}/${board.recentLessons}`
|
||||||
|
: "—"
|
||||||
}}
|
}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
v-model:show="leaveDialog"
|
||||||
|
preset="card"
|
||||||
|
:title="
|
||||||
|
leaveCandidates.length
|
||||||
|
? `今天还没交过的 ${leaveCandidates.length} 人里,谁请假了?`
|
||||||
|
: '今天全班都交过题了'
|
||||||
|
"
|
||||||
|
style="width: 640px; max-width: calc(100vw - 32px)"
|
||||||
|
>
|
||||||
|
<template v-if="leaveCandidates.length">
|
||||||
|
<n-text depth="3" class="meta">
|
||||||
|
勾上不在的人,看板就不算他们。<template
|
||||||
|
v-if="boardStudents.length > leaveCandidates.length"
|
||||||
|
>其余
|
||||||
|
{{ boardStudents.length - leaveCandidates.length }} 人今天交过题,不用标。</template
|
||||||
|
>
|
||||||
|
</n-text>
|
||||||
|
<n-checkbox-group v-model:value="leaveSelected">
|
||||||
|
<div class="leave-grid">
|
||||||
|
<n-checkbox
|
||||||
|
v-for="student in leaveCandidates"
|
||||||
|
:key="student.userId"
|
||||||
|
:value="student.username"
|
||||||
|
:label="nameOf(student)"
|
||||||
|
class="leave-option"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-checkbox-group>
|
||||||
|
</template>
|
||||||
|
<n-text v-else depth="3">请假的人一定是没交过题的,现在没有人可以标。</n-text>
|
||||||
|
<template #footer>
|
||||||
|
<n-flex align="center" :wrap="false">
|
||||||
|
<n-text depth="3" class="meta" style="flex-grow: 1">
|
||||||
|
已选 {{ leaveSelected.length }} 人 · 只记在这台电脑上,今天有效
|
||||||
|
</n-text>
|
||||||
|
<n-button :disabled="!leaveSelected.length" @click="leaveSelected = []">
|
||||||
|
全部取消
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="leaveDialog = false">取消</n-button>
|
||||||
|
<n-button type="primary" @click="saveLeave">确定</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -525,8 +615,40 @@ function submissionsHref(
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-bar {
|
.leave-button {
|
||||||
margin-bottom: 6px;
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leave-names {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leave-edit {
|
||||||
|
margin-left: 8px;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leave-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 10px 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leave-option {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid .leave-sep td {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.6;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid .leave-row td {
|
||||||
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board {
|
.board {
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { onMounted, ref } from "vue"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 课堂看板的「请假隐藏」:暂时把某个学生从「今天还没交过」里藏起来。
|
|
||||||
*
|
|
||||||
* 老师是把看板投在屏幕上盯着看的,没交名单里总有那么几个是请假/转班/学号错了的,
|
|
||||||
* 一直挂在上面会盖住真正需要盯的人。隐藏是**带过期时间**的(两小时,够一节课),
|
|
||||||
* 不是永久删除 —— 下节课自动回来,免得有人被无声地漏掉。
|
|
||||||
*
|
|
||||||
* 存在 localStorage 而不是后端:这是「这台电脑上这位老师这节课不想看谁」,
|
|
||||||
* 换个人、换台机器都不该继承。
|
|
||||||
*
|
|
||||||
* 原来在提交统计、流程图统计两个弹框里;统计改成回头看的页面之后(2026-09),
|
|
||||||
* 课上盯人的事归看板,这个也挪了过来。
|
|
||||||
*
|
|
||||||
* @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 unhide(usernames: string[]) {
|
|
||||||
const drop = new Set(usernames)
|
|
||||||
hiddenStudents.value = Object.fromEntries(
|
|
||||||
Object.entries(hiddenStudents.value).filter(([name]) => !drop.has(name)),
|
|
||||||
)
|
|
||||||
save(hiddenStudents.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHidden(username: string) {
|
|
||||||
const expiresAt = hiddenStudents.value[username]
|
|
||||||
return !!expiresAt && expiresAt > Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
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, unhide, isHidden }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { onMounted, ref } from "vue"
|
||||||
|
import { parseTime } from "utils/functions"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课堂看板的「请假」:老师手动标记今天不在的学生,看板的人数、进度、「还没交过」都不算他们。
|
||||||
|
*
|
||||||
|
* **今天有效**(按东八区的日子),第二天自动清掉 —— 请假一般是请一整天,原来按两小时算,
|
||||||
|
* 连上两节课的第二节中途人又冒出来,得重标一遍。
|
||||||
|
*
|
||||||
|
* 存在 localStorage 而不是后端:这是「这台电脑上这位老师今天不想算谁」,
|
||||||
|
* 换个人、换台机器都不该继承(2026-10 用户定的,不上服务器)。
|
||||||
|
*
|
||||||
|
* 原来叫「请假隐藏」(开关 → 名字变成带 × 的标签 → 点 ×),用户说不会用,2026-10 改成
|
||||||
|
* 看板上一个「请假」按钮勾名单。旧键 `oj_hidden_students` 存的是两小时的过期时刻,
|
||||||
|
* 口径不同,不迁移,直接删掉。
|
||||||
|
*
|
||||||
|
* @param storageKey localStorage 的键
|
||||||
|
*/
|
||||||
|
export function useLeaveStudents(storageKey: string) {
|
||||||
|
/** 用户名 → 标记的那一天(YYYY-MM-DD,东八区) */
|
||||||
|
type Marks = Record<string, string>
|
||||||
|
|
||||||
|
function today() {
|
||||||
|
return parseTime(new Date(), "YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(): Marks {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(storageKey) ?? "{}")
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(data: Marks) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(data))
|
||||||
|
} catch {
|
||||||
|
// 存不下就只在这次打开里有效,不影响看板
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const marks = ref<Marks>(load())
|
||||||
|
|
||||||
|
function onLeave(username: string) {
|
||||||
|
return marks.value[username] === today()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 `usernames` 这批人的标记换成 `selected`:在里面但没选的取消,选了的标上。
|
||||||
|
* 只动这一批(看板上这个班),别的班标着的人不受影响
|
||||||
|
*/
|
||||||
|
function setLeave(usernames: string[], selected: string[]) {
|
||||||
|
const scope = new Set(usernames)
|
||||||
|
const day = today()
|
||||||
|
const next = Object.fromEntries(
|
||||||
|
Object.entries(marks.value).filter(([name]) => !scope.has(name)),
|
||||||
|
)
|
||||||
|
for (const name of selected) next[name] = day
|
||||||
|
marks.value = next
|
||||||
|
save(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelLeave(usernames: string[]) {
|
||||||
|
setLeave(usernames, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem("oj_hidden_students")
|
||||||
|
} catch {
|
||||||
|
// 读写不了 localStorage 的环境,没什么可清的
|
||||||
|
}
|
||||||
|
// 把不是今天的清掉再落一次盘,否则这张表只增不减
|
||||||
|
const day = today()
|
||||||
|
const cleaned = Object.fromEntries(
|
||||||
|
Object.entries(marks.value).filter(([, marked]) => marked === day),
|
||||||
|
)
|
||||||
|
marks.value = cleaned
|
||||||
|
save(cleaned)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { onLeave, setLeave, cancelLeave }
|
||||||
|
}
|
||||||
Reference in new issue
Block a user