Files
OJ2/apps/web/src/shared/store/lesson.ts
T
xuyueandClaude Opus 5.5 c3f1413545 fix(课堂任务): 后台改一节不再把它排到同一分钟布置的另一节前面;学生管理员算进统计的课
- 后台「改」时刻只填到分,原来一保存就把布置时刻截成整分:先画流程图、紧接着再开一节写代码,
  只改了做什么,「今天最后一节」就换回了画流程图那节。没改时刻就留着原来的
- 数据统计「一行一节课」只数 Regular User,学生管理员的提交不算,和「做完 / 全班」的花名册对不上
- 学生「老师改了这节课」:画流程图改成写 C、先画再写改成写代码,原来写「语言 改成 C语言」,
  看不出不用画了;现在写「做法 改成 写代码 · C语言」
- 学生课堂标题按每道题合起来的要求写:先画流程图、再用这几道题写代码时,不再只写「用 C语言做」
- 先画再写的题,切到别的题再回来不再被拽回画布(原来只记最后一道题)
- 课堂任务顶上写代码的课也写「写代码」;讲错误的弹层开着时不再因为 15 秒刷新换了个人

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:26:39 +08:00

115 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { LESSON_MODE_LABEL, type LessonMode } from '@oj2/contract'
import { defineStore } from 'pinia'
import { getClassActivity } from 'shared/api'
import { LANGUAGE_SHOW_VALUE } from 'utils/constants'
import type { ClassActivity, ClassActivityProblem } from 'utils/types'
/**
* 学生这边这节课做什么:按每道题合起来的要求算(后端 todaySteps 把今天几节里同一道题的要求并在一起),
* 不直接用 `mode` —— 那是今天最后布置的那一节的。先布置画流程图、再「用这几道题写代码」,
* 最后一节是写代码,但还没画完的照样要画,标题只写「用 C语言做」就漏了画图。
* 推断出来的课为 null
*/
export function activityMode(activity: ClassActivity | null | undefined): LessonMode | null {
if (!activity?.mode) return null
const draw = activity.problems.some((item) => item.draw !== null)
const code = activity.problems.some((item) => item.code !== null)
return draw && code ? 'both' : draw ? 'flowchart' : code ? 'code' : activity.mode
}
/** 同一节课里来回换题不用每次都重拉;过了这么久再进题目页就重拉一次 */
const STALE_MS = 2 * 60 * 1000
/**
* 「这节课的题」:首页「班里在做」那几道(老师在课堂任务布置的,或者从同班提交推断的)。
*
* 题目页有两处要用:顶部的课堂条(做完几道、每道的状态),和通过之后的「下一题」。
* 原来「下一题」每次通过都自己拉一遍,现在两处读同一份,通过之后由判题那边
* (SubmissionEffects)先把这道就地标成做完、再重拉一次。
*/
export const useLessonStore = defineStore('lesson', () => {
const activity = ref<ClassActivity | null>(null)
let loadedAt = 0
let loading: Promise<void> | null = null
/**
* 只认最后发出去的那次。进页面那次还在路上时学生就交对了:先就地标成做完、再强制重拉 ——
* 先发的那次要是后到,会把「做完」盖回去,「下一题」又指回刚做完的这道
*/
let sequence = 0
/** 没拉过或者旧了才拉;force 用在刚通过一道题之后 */
function load(force = false) {
if (!force && activity.value && Date.now() - loadedAt < STALE_MS) return loading
if (!force && loading) return loading
const mine = ++sequence
loading = getClassActivity()
.then((res) => {
if (mine !== sequence) return
activity.value = res
loadedAt = Date.now()
})
// 拉不到就没有课堂条,不影响做题
.catch(() => {})
.finally(() => {
if (mine === sequence) loading = null
})
return loading
}
/**
* 刚做对:不等重拉,先把这道标上,课堂条上的「做完 N/M」马上加一。`step` 是做对的哪一步:
* 代码交对、还是流程图画到 A / S。老师布置的课要这道题画和写两步时,只做对一步还不算做完;
* 推断出来的课不分步(draw / code 都是 null),哪样做对都算
*/
function markAccepted(problemDisplayId: string, step: 'code' | 'draw') {
const item = find(problemDisplayId)
if (!item) return
if (step === 'code' && item.code) item.code = 'accepted'
if (step === 'draw' && item.draw) item.draw = 'accepted'
const steps = [item.draw, item.code].filter((value) => value !== null)
// 这节课不要这一步(只画流程图的课交对了代码):不算
if (steps.length && (step === 'code' ? !item.code : !item.draw)) return
item.myStatus = steps.every((value) => value === 'accepted') ? 'accepted' : 'tried'
}
function find(problemDisplayId: string): ClassActivityProblem | undefined {
const id = problemDisplayId.toLowerCase()
return activity.value?.problems.find((item) => item.problemDisplayId.toLowerCase() === id)
}
/** 这道题在不在这节课里;不在(学生在做别的)就没有课堂条、也没有「下一题」 */
function includes(problemDisplayId: string) {
return !!find(problemDisplayId)
}
/**
* 通过这道之后该去哪一道:按列表顺序排在这道**后面**、还没通过的第一道,后面都做完了
* 再从头找 —— 学生常常跳着做,先做完了第 3 道再回头做第 1 道。当前这道本身不看它的
* myStatus:重拉回来之前,它还是通过之前那一刻的状态。
* 这道不在这节课里返回 null;在,但别的都做完了,next 为 null。
*/
function nextAfter(problemDisplayId: string) {
const list = activity.value?.problems ?? []
const id = problemDisplayId.toLowerCase()
const index = list.findIndex((item) => item.problemDisplayId.toLowerCase() === id)
if (index < 0) return null
const rest = [...list.slice(index + 1), ...list.slice(0, index)].filter((item) => item.myStatus !== 'accepted')
return { total: list.length, next: rest[0] ?? null, remaining: rest.length }
}
/**
* 老师布置时选的做法,给界面写「画流程图」「先画再写 · C语言」「C语言」;没选(推断出来的课、
* 以前布置的)为空。写代码是默认的,不写出来
*/
const languageText = computed(() => {
const mode = activityMode(activity.value)
const language = mode === 'flowchart' ? null : activity.value?.language
return [mode && mode !== 'code' ? LESSON_MODE_LABEL[mode] : '', language ? LANGUAGE_SHOW_VALUE[language] : ''].filter(Boolean).join(' · ')
})
/** 老师布置的叫「老师布置的题」(选了语言带上语言),推断出来的叫「这节课的题」 */
const label = computed(() => (activity.value?.source === 'teacher' ? `老师布置的题${languageText.value ? ` · ${languageText.value}` : ''}` : '这节课的题'))
return { activity, label, languageText, load, markAccepted, find, includes, nextAfter }
})