feat(统计): 数据统计从弹框改成独立页面,左边题目、右边学生,一道题看全班每人的每一次
- 课堂看板管这节课,统计管回头看:时间段默认「两节课」,还有这节课、今天、7 天、30 天、这学期、全部、自己选日子;不再自动刷新 - 选一道题看每人在这道题上的方块串,鼠标停上去看那次的代码;选「全部」每人一行、展开按题看 - 没选班级(从题目页进来)先按班级汇总;流程图统计有等级分布、四项雷达、各项平均分、词云和每人最好的一次 - 提交列表、题目页的「数据统计」改成新标签打开这个页面 - 「请假隐藏」挪到课堂看板:点名字旁的 × 藏两个小时,做完人数的分母一起减掉 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f09da88ff7
commit
ed55ee5af6
16 files changed
+2256
-76
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import { TIME_ZONE_OFFSET_MINUTES } from "@oj2/contract"
|
||||
import { parseTime, zonedParts } from "utils/functions"
|
||||
|
||||
const OFFSET_MS = TIME_ZONE_OFFSET_MINUTES * 60_000
|
||||
const HOUR = 3_600_000
|
||||
const DAY = 86_400_000
|
||||
|
||||
/**
|
||||
* 统计页的时间段。课堂统计一般看一两节课(用户原话),所以这两档在最前面、
|
||||
* 默认「两节课」;后面几档是回头看。原来弹框里的 10 / 20 / 30 分钟是上课盯人用的,
|
||||
* 那件事现在归课堂看板。
|
||||
*/
|
||||
export const PERIOD_OPTIONS = [
|
||||
{ label: "这节课(1 小时内)", value: "1h" },
|
||||
{ label: "两节课(2 小时内)", value: "2h" },
|
||||
{ label: "今天", value: "today" },
|
||||
{ label: "最近 7 天", value: "7d" },
|
||||
{ label: "最近 30 天", value: "30d" },
|
||||
{ label: "这学期", value: "term" },
|
||||
{ label: "全部", value: "all" },
|
||||
{ label: "自己选日子", value: "custom" },
|
||||
] as const
|
||||
|
||||
export type Period = (typeof PERIOD_OPTIONS)[number]["value"]
|
||||
export const DEFAULT_PERIOD: Period = "2h"
|
||||
|
||||
/** 东八区某天 0 点对应的 UTC 时刻 */
|
||||
function dayStartOf(year: number, month: number, day: number) {
|
||||
return Date.UTC(year, month - 1, day) - OFFSET_MS
|
||||
}
|
||||
|
||||
/** 东八区「现在」这一天的 0 点 */
|
||||
function todayStart(now: number) {
|
||||
const p = zonedParts(new Date(now))!
|
||||
return dayStartOf(p.year, p.month, p.day)
|
||||
}
|
||||
|
||||
/** 这学期从哪天起:9 月到次年 2 月算秋季学期(9 月 1 日起),3 到 8 月算春季(3 月 1 日起) */
|
||||
function termStart(now: number) {
|
||||
const p = zonedParts(new Date(now))!
|
||||
if (p.month >= 9) return dayStartOf(p.year, 9, 1)
|
||||
if (p.month >= 3) return dayStartOf(p.year, 3, 1)
|
||||
return dayStartOf(p.year - 1, 9, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间段换成接口要的 start / end(ISO),外加一句给人看的范围。
|
||||
* `custom` 用的是 [开始日 0 点, 结束日 24 点),两个都是 date picker 取回来的 UTC 时刻。
|
||||
*/
|
||||
export function periodRange(period: Period, custom: [number, number] | null, now = Date.now()) {
|
||||
let start: number | null
|
||||
let end = now
|
||||
switch (period) {
|
||||
case "1h":
|
||||
start = now - HOUR
|
||||
break
|
||||
case "2h":
|
||||
start = now - 2 * HOUR
|
||||
break
|
||||
case "today":
|
||||
start = todayStart(now)
|
||||
break
|
||||
case "7d":
|
||||
start = todayStart(now) - 6 * DAY
|
||||
break
|
||||
case "30d":
|
||||
start = todayStart(now) - 29 * DAY
|
||||
break
|
||||
case "term":
|
||||
start = termStart(now)
|
||||
break
|
||||
case "custom":
|
||||
if (custom) {
|
||||
start = custom[0]
|
||||
end = custom[1] + DAY
|
||||
} else {
|
||||
start = todayStart(now)
|
||||
}
|
||||
break
|
||||
default:
|
||||
start = null
|
||||
}
|
||||
const text = describe(start, end, period)
|
||||
return {
|
||||
start: start === null ? undefined : new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
text,
|
||||
/** 范围落在今天以内:提交列表那边能用「今天」接上 */
|
||||
withinToday: start !== null && start >= todayStart(now),
|
||||
}
|
||||
}
|
||||
|
||||
function describe(start: number | null, end: number, period: Period) {
|
||||
if (start === null) return ""
|
||||
if (period === "1h" || period === "2h") {
|
||||
return `${parseTime(new Date(start), "HH:mm")}–${parseTime(new Date(end), "HH:mm")}`
|
||||
}
|
||||
const from = parseTime(new Date(start), "M月D日")
|
||||
const to = parseTime(new Date(end - 1), "M月D日")
|
||||
return from === to ? from : `${from}–${to}`
|
||||
}
|
||||
Reference in new issue
Block a user