feat(统计): 没填班级的学生也能和班级一样看——班级下拉多一项「没填班级」,一起上的课能切出来、点进去看小方块;零散提交也能点了

- 班级筛选多一个取值 NO_CLASS("none"),统计、方块串、流程图、提交列表都认;只圈普通学生,
  老师管理员的号本来就不填班级,不挡的话试题的提交会算进去
- 切课时没填班级的号合在一起当一个班切(原来 class_name is null 直接排除,7 月 4 日那两节课看不到)
- 没填班级不是真班:不列「还没交」,课上只写几人不写几人之几
- 「另有零散提交」每一项可点,进那个班那一整天

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-30 12:18:06 +08:00
1 parent c2ef55b10b
commit 923f3efeca
10 files changed
+153 -66

No files matched your search

@@ -1,11 +1,12 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui"
import type {
FlowchartStatistics,
SubmissionLessons,
SubmissionStatistics,
SubmissionStatisticsGrid,
import {
NO_CLASS,
type FlowchartStatistics,
type SubmissionLessons,
type SubmissionStatistics,
type SubmissionStatisticsGrid,
} from "@oj2/contract"
import {
getFlowchartStatistics,
@@ -16,8 +17,8 @@ import {
import { useConfigStore } from "shared/store/config"
import { fromPickerValue, toPickerValue } from "utils/functions"
import { useTone } from "oj/submission/composables/tone"
import { classLabel } from "oj/submission/utils"
import { DEFAULT_PERIOD, PERIOD_OPTIONS, periodRange, type Period } from "../period"
import { classSelectOptions } from "oj/submission/utils"
import { DEFAULT_PERIOD, PERIOD_OPTIONS, dayStartOfDate, periodRange, type Period } from "../period"
import { resultName, useResultColor } from "../results"
import ByClass from "./ByClass.vue"
import CodeStats from "./CodeStats.vue"
@@ -106,11 +107,9 @@ function pickDates(value: [number, number] | null) {
/** 学生框:包含匹配。班级另外单独传(按 class_name 精确匹配,见后端 scopedUsers) */
const studentParam = computed(() => username.value.trim())
const classOptions = computed(() => {
const list = configStore.config?.classList ?? []
const all = className.value && !list.includes(className.value) ? [className.value, ...list] : list
return all.map((item) => ({ label: classLabel(item), value: item }))
})
const classOptions = computed(() =>
classSelectOptions(configStore.config?.classList ?? [], className.value),
)
// ---------- 取数 ----------
@@ -306,7 +305,7 @@ function pickClass(value: string) {
/** 点一节课:这个班、这节课的题、这节课第一条到最后一条(多留 1 秒:库里存到微秒,转成毫秒会截掉零头) */
function pickLesson(lesson: SubmissionLessons["lessons"][number]) {
remember()
className.value = lesson.className
className.value = lesson.className ?? NO_CLASS
username.value = ""
problem.value = lesson.problems.join(" ")
from.value = String(Date.parse(lesson.start))
@@ -314,6 +313,18 @@ function pickLesson(lesson: SubmissionLessons["lessons"][number]) {
period.value = "lesson"
}
/** 点某天的零散提交:这个班(或没填班级的)那一整天,题号不限 —— 补做、课后练的都在里面 */
function pickScattered(item: SubmissionLessons["scattered"][number]) {
remember()
className.value = item.className ?? NO_CLASS
username.value = ""
problem.value = ""
const day = String(dayStartOfDate(item.day))
from.value = day
to.value = day
period.value = "custom"
}
/** 点「错得最多的题」里的一道:只看这道,按班级汇总 */
function pickProblem(displayId: string) {
remember()
@@ -478,6 +489,7 @@ function resultWidth(value: number) {
:timeline="shown.withinToday"
:loading-more="loadingMore"
@pick="pickLesson"
@scattered="pickScattered"
@problem="pickProblem"
@more="loadMore"
/>