fix(课堂任务): 后台改一节不再把它排到同一分钟布置的另一节前面;学生管理员算进统计的课
- 后台「改」时刻只填到分,原来一保存就把布置时刻截成整分:先画流程图、紧接着再开一节写代码, 只改了做什么,「今天最后一节」就换回了画流程图那节。没改时刻就留着原来的 - 数据统计「一行一节课」只数 Regular User,学生管理员的提交不算,和「做完 / 全班」的花名册对不上 - 学生「老师改了这节课」:画流程图改成写 C、先画再写改成写代码,原来写「语言 改成 C语言」, 看不出不用画了;现在写「做法 改成 写代码 · C语言」 - 学生课堂标题按每道题合起来的要求写:先画流程图、再用这几道题写代码时,不再只写「用 C语言做」 - 先画再写的题,切到别的题再回来不再被拽回画布(原来只记最后一道题) - 课堂任务顶上写代码的课也写「写代码」;讲错误的弹层开着时不再因为 15 秒刷新换了个人 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b09d479893
commit
c3f1413545
7 files changed
+67
-25
No files matched your search
@@ -161,10 +161,14 @@ adminLessonRoutes.put('/lessons/:id', requireTeacher, async (c) => {
|
|||||||
// 班级能改:原来那个班和新的班学生看到的都可能变,两边各比一次
|
// 班级能改:原来那个班和新的班学生看到的都可能变,两边各比一次
|
||||||
const classes = await affectedClasses(id, result.values.className)
|
const classes = await affectedClasses(id, result.values.className)
|
||||||
const before = await Promise.all(classes.map((className) => todayLessonSnapshot(className)))
|
const before = await Promise.all(classes.map((className) => todayLessonSnapshot(className)))
|
||||||
|
// 时刻只填到分:没改时刻就留着原来带秒的那个。不然只改了做什么,这一节也被挪到这一分钟的开头,
|
||||||
|
// 排到同一分钟里后布置的那节前面去 —— 「今天最后一节」就换了人(先画流程图、紧接着再开一节写代码,常在同一分钟里)
|
||||||
|
const [current] = await db.select({ createdAt: schema.classLesson.createdAt }).from(schema.classLesson).where(eq(schema.classLesson.id, id))
|
||||||
|
const sameMinute = current && Math.floor(Date.parse(current.createdAt) / 60_000) * 60_000 === Date.parse(result.values.createdAt)
|
||||||
// 布置的老师不换,和任务页上「改」一样认第一次
|
// 布置的老师不换,和任务页上「改」一样认第一次
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.update(schema.classLesson)
|
.update(schema.classLesson)
|
||||||
.set({ ...result.values, updatedAt: new Date().toISOString() })
|
.set({ ...result.values, createdAt: sameMinute ? current.createdAt : result.values.createdAt, updatedAt: new Date().toISOString() })
|
||||||
.where(eq(schema.classLesson.id, id))
|
.where(eq(schema.classLesson.id, id))
|
||||||
.returning({ id: schema.classLesson.id })
|
.returning({ id: schema.classLesson.id })
|
||||||
if (!row) return failure(c, 404, 'lesson-not-found', '这一节已经被删掉了')
|
if (!row) return failure(c, 404, 'lesson-not-found', '这一节已经被删掉了')
|
||||||
|
|||||||
@@ -527,6 +527,11 @@ const LESSON_LIVE_MINUTES = 15
|
|||||||
/** 一次列多少节课。「一节课」「今天」一般一两行,「这学期」「全部」有几百节,先给最近的这些 */
|
/** 一次列多少节课。「一节课」「今天」一般一两行,「这学期」「全部」有几百节,先给最近的这些 */
|
||||||
const LESSONS_DEFAULT = 50
|
const LESSONS_DEFAULT = 50
|
||||||
const LESSONS_MAX = 500
|
const LESSONS_MAX = 500
|
||||||
|
/** 课只算学生(STUDENT_ROLES,学生管理员也算),老师试题的提交不算 */
|
||||||
|
const studentRoles = sql.join(
|
||||||
|
STUDENT_ROLES.map((role) => sql`${role}`),
|
||||||
|
sql`, `,
|
||||||
|
)
|
||||||
|
|
||||||
type StatisticsRange = { start: string | null; end: string }
|
type StatisticsRange = { start: string | null; end: string }
|
||||||
|
|
||||||
@@ -599,7 +604,7 @@ async function assignedLessons(plans: LessonPlan[], range: StatisticsRange) {
|
|||||||
where s.contest_id is null and p.mode <> 'flowchart'
|
where s.contest_id is null and p.mode <> 'flowchart'
|
||||||
and s.create_time <= ${range.end}
|
and s.create_time <= ${range.end}
|
||||||
${range.start ? sql`and s.create_time >= ${range.start}` : sql``}
|
${range.start ? sql`and s.create_time >= ${range.start}` : sql``}
|
||||||
and u.admin_type = 'Regular User' and u.is_disabled = false
|
and u.admin_type in (${studentRoles}) and u.is_disabled = false
|
||||||
union all
|
union all
|
||||||
select p.k, f.user_id, f.problem_id, (f.status = 2 and coalesce(f.ai_grade in (${sql.join(
|
select p.k, f.user_id, f.problem_id, (f.status = 2 and coalesce(f.ai_grade in (${sql.join(
|
||||||
FLOWCHART_PASS_GRADES.map((grade) => sql`${grade}`),
|
FLOWCHART_PASS_GRADES.map((grade) => sql`${grade}`),
|
||||||
@@ -612,7 +617,7 @@ async function assignedLessons(plans: LessonPlan[], range: StatisticsRange) {
|
|||||||
where p.mode <> 'code'
|
where p.mode <> 'code'
|
||||||
and f.create_time <= ${range.end}
|
and f.create_time <= ${range.end}
|
||||||
${range.start ? sql`and f.create_time >= ${range.start}` : sql``}
|
${range.start ? sql`and f.create_time >= ${range.start}` : sql``}
|
||||||
and u.admin_type = 'Regular User' and u.is_disabled = false
|
and u.admin_type in (${studentRoles}) and u.is_disabled = false
|
||||||
)
|
)
|
||||||
select k, problem_id,
|
select k, problem_id,
|
||||||
count(distinct user_id)::int as users,
|
count(distinct user_id)::int as users,
|
||||||
@@ -721,8 +726,8 @@ submissionStatisticsRoutes.get('/submissions/statistics/lessons', requireTeacher
|
|||||||
where s.contest_id is null
|
where s.contest_id is null
|
||||||
and s.create_time <= ${range.end}
|
and s.create_time <= ${range.end}
|
||||||
${range.start ? sql`and s.create_time >= ${range.start}` : sql``}
|
${range.start ? sql`and s.create_time >= ${range.start}` : sql``}
|
||||||
-- 只算普通学生:老师试题的提交不该把一段拼成「一节课」
|
-- 只算学生(学生管理员也是学生):老师试题的提交不该把一段拼成「一节课」
|
||||||
and u.admin_type = 'Regular User' and u.is_disabled = false
|
and u.admin_type in (${studentRoles}) and u.is_disabled = false
|
||||||
),
|
),
|
||||||
g as (
|
g as (
|
||||||
select *,
|
select *,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getClassActivity } from 'shared/api'
|
|||||||
import SubmissionResultTag from 'shared/components/SubmissionResultTag.vue'
|
import SubmissionResultTag from 'shared/components/SubmissionResultTag.vue'
|
||||||
import { pickHeadline, type Headline } from 'oj/user/knowledge'
|
import { pickHeadline, type Headline } from 'oj/user/knowledge'
|
||||||
import { useUserStore } from 'shared/store/user'
|
import { useUserStore } from 'shared/store/user'
|
||||||
|
import { activityMode } from 'shared/store/lesson'
|
||||||
import { ContestStatus, CONTEST_STATUS, JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from 'utils/constants'
|
import { ContestStatus, CONTEST_STATUS, JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from 'utils/constants'
|
||||||
import { duration, parseTime, zonedParts, zonedYear } from 'utils/functions'
|
import { duration, parseTime, zonedParts, zonedYear } from 'utils/functions'
|
||||||
import type { AnnouncementListItem, ClassActivity, ClassActivityProblem, Contest, LastVisit, SubmissionListItem, WeeklyRank } from 'utils/types'
|
import type { AnnouncementListItem, ClassActivity, ClassActivityProblem, Contest, LastVisit, SubmissionListItem, WeeklyRank } from 'utils/types'
|
||||||
@@ -191,7 +192,7 @@ const classActivityTitle = computed(() => {
|
|||||||
// 老师在课堂任务布置的,说成「老师布置的」,比「班里在做」更让人知道该做这几道
|
// 老师在课堂任务布置的,说成「老师布置的」,比「班里在做」更让人知道该做这几道
|
||||||
if (classActivity.value?.source === 'teacher') {
|
if (classActivity.value?.source === 'teacher') {
|
||||||
const language = classActivity.value.language
|
const language = classActivity.value.language
|
||||||
const mode = classActivity.value.mode
|
const mode = activityMode(classActivity.value)
|
||||||
if (mode === 'flowchart') return '这节课 · 画流程图'
|
if (mode === 'flowchart') return '这节课 · 画流程图'
|
||||||
if (mode === 'both') return `这节课 · 先画流程图,再${language ? `用${LANGUAGE_SHOW_VALUE[language]}` : ''}写代码`
|
if (mode === 'both') return `这节课 · 先画流程图,再${language ? `用${LANGUAGE_SHOW_VALUE[language]}` : ''}写代码`
|
||||||
return `这节课${language ? ` · 用${LANGUAGE_SHOW_VALUE[language]}做` : ''}`
|
return `这节课${language ? ` · 用${LANGUAGE_SHOW_VALUE[language]}做` : ''}`
|
||||||
|
|||||||
@@ -182,16 +182,25 @@ watch(
|
|||||||
* 没有画这一步的课,画着流程图时不动他
|
* 没有画这一步的课,画着流程图时不动他
|
||||||
*/
|
*/
|
||||||
const lessonStore = useLessonStore()
|
const lessonStore = useLessonStore()
|
||||||
let lessonLanguageFor: string | null = null
|
/**
|
||||||
|
* 换过的(题 + 这一节 + 该做的那一步)都记下:换到别的题再回来,同一步不再换第二次。
|
||||||
|
* 编辑器在换题时不重新挂载(detail.vue 只重新 init),只记最后一个的话一回来又被拽回去
|
||||||
|
*/
|
||||||
|
const lessonSwitched = new Set<string>()
|
||||||
|
let lessonViewing: string | null = null
|
||||||
watch(
|
watch(
|
||||||
() => [problem.value?._id, lessonStore.activity] as const,
|
() => [problem.value?._id, lessonStore.activity] as const,
|
||||||
([id, activity]) => {
|
([id, activity]) => {
|
||||||
if (!id || !activity) return
|
if (!id || !activity) return
|
||||||
const key = [id, activity.day, activity.mode, activity.language].join('|')
|
const lesson = [id, activity.day, activity.mode, activity.language].join('|')
|
||||||
if (lessonLanguageFor === key) return
|
|
||||||
lessonLanguageFor = key
|
|
||||||
if (ctx.value.entry !== 'problem' || teacherCollab.value) return
|
|
||||||
const item = lessonStore.find(id)
|
const item = lessonStore.find(id)
|
||||||
|
const key = [lesson, item?.draw && item.draw !== 'accepted' ? 'draw' : 'code'].join('|')
|
||||||
|
// 停在这道题上、只是课堂状态刷新了(比如刚画对图):不跟着换,也不记,下回打开这道题再换
|
||||||
|
const stay = lessonViewing === lesson
|
||||||
|
lessonViewing = lesson
|
||||||
|
if (lessonSwitched.has(key) || stay) return
|
||||||
|
lessonSwitched.add(key)
|
||||||
|
if (ctx.value.entry !== 'problem' || teacherCollab.value) return
|
||||||
if (!item) return
|
if (!item) return
|
||||||
if (item.draw && item.draw !== 'accepted') {
|
if (item.draw && item.draw !== 'accepted') {
|
||||||
if (problemStore.canDraw && codeStore.code.language !== 'Flowchart') problemStore.switchLanguage('Flowchart')
|
if (problemStore.canDraw && codeStore.code.language !== 'Flowchart') problemStore.switchLanguage('Flowchart')
|
||||||
|
|||||||
@@ -408,10 +408,14 @@ const explainShow = ref(false)
|
|||||||
const explainIndex = ref(0)
|
const explainIndex = ref(0)
|
||||||
const explainProblemIndex = ref(-1)
|
const explainProblemIndex = ref(-1)
|
||||||
const explainProblem = computed(() => board.value?.problems[explainProblemIndex.value] ?? null)
|
const explainProblem = computed(() => board.value?.problems[explainProblemIndex.value] ?? null)
|
||||||
/** 弹层顶上那排人:这道题错着的人,顺序和错法堆一样 */
|
/**
|
||||||
const explainPeople = computed<ExplainPerson[]>(() => {
|
* 弹层顶上那排人:这道题错着的人,顺序和错法堆一样。打开那一刻定下来,开着时不跟 15 秒的刷新变:
|
||||||
if (explainProblemIndex.value < 0) return []
|
* 弹层记的是第几个人,名单一变(有人改对了、图评完了回到名单里)就成了讲着讲着换了个人
|
||||||
const people = wrongOf(explainProblemIndex.value).filter((person) => person.explain)
|
*/
|
||||||
|
const explainPeople = ref<ExplainPerson[]>([])
|
||||||
|
function explainPeopleOf(problemIndex: number): ExplainPerson[] {
|
||||||
|
if (problemIndex < 0) return []
|
||||||
|
const people = wrongOf(problemIndex).filter((person) => person.explain)
|
||||||
const order = new Map<string, number>()
|
const order = new Map<string, number>()
|
||||||
for (const person of people) {
|
for (const person of people) {
|
||||||
order.set(person.kind.key, (order.get(person.kind.key) ?? 0) + 1)
|
order.set(person.kind.key, (order.get(person.kind.key) ?? 0) + 1)
|
||||||
@@ -423,10 +427,11 @@ const explainPeople = computed<ExplainPerson[]>(() => {
|
|||||||
? { userId: person.student.userId, username: person.student.username, flowchartId: person.cell.flowchart!.latest!.id, label: person.kind.label }
|
? { userId: person.student.userId, username: person.student.username, flowchartId: person.cell.flowchart!.latest!.id, label: person.kind.label }
|
||||||
: { userId: person.student.userId, username: person.student.username, submissionId: person.cell.latest!.submissionId, label: person.kind.label },
|
: { userId: person.student.userId, username: person.student.username, submissionId: person.cell.latest!.submissionId, label: person.kind.label },
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
function openExplain(problemIndex: number, userId: number) {
|
function openExplain(problemIndex: number, userId: number) {
|
||||||
explainProblemIndex.value = problemIndex
|
explainProblemIndex.value = problemIndex
|
||||||
|
explainPeople.value = explainPeopleOf(problemIndex)
|
||||||
explainIndex.value = Math.max(
|
explainIndex.value = Math.max(
|
||||||
0,
|
0,
|
||||||
explainPeople.value.findIndex((person) => person.userId === userId),
|
explainPeople.value.findIndex((person) => person.userId === userId),
|
||||||
@@ -532,7 +537,8 @@ function lastText(student: TaskBoardStudent) {
|
|||||||
/>
|
/>
|
||||||
<div v-else-if="board?.problems.length && !needLesson" class="lesson">
|
<div v-else-if="board?.problems.length && !needLesson" class="lesson">
|
||||||
<span class="lesson-label">{{ board.source === 'teacher' ? '这节课' : '今天在做' }}</span>
|
<span class="lesson-label">{{ board.source === 'teacher' ? '这节课' : '今天在做' }}</span>
|
||||||
<span v-if="board.mode && board.mode !== 'code'" class="chip lang">{{ LESSON_MODE_LABEL[board.mode] }}</span>
|
<!-- 写代码也写出来:只写「Python」的话,看不出这节是写代码还是先画再写 -->
|
||||||
|
<span v-if="board.mode" class="chip lang">{{ LESSON_MODE_LABEL[board.mode] }}</span>
|
||||||
<span v-if="board.language" class="chip lang">{{ LANGUAGE_SHOW_VALUE[board.language] }}</span>
|
<span v-if="board.language" class="chip lang">{{ LANGUAGE_SHOW_VALUE[board.language] }}</span>
|
||||||
<span v-for="problem in board.problems" :key="problem.problemId" class="chip">
|
<span v-for="problem in board.problems" :key="problem.problemId" class="chip">
|
||||||
{{ problem.problemDisplayId }}
|
{{ problem.problemDisplayId }}
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ function names(list: Item[]) {
|
|||||||
return list.map((item) => `${item.problemDisplayId} ${item.title}`).join('、')
|
return list.map((item) => `${item.problemDisplayId} ${item.title}`).join('、')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 「画流程图」「先画再写 · C语言」「C语言」:写代码是默认的,不写出来 */
|
/**
|
||||||
function languageText(value: LessonUpdate) {
|
* 「画流程图」「先画再写 · C语言」「C语言」。新布置的一节写代码是默认的,不写出来(前面配「用」);
|
||||||
const mode = value.mode && value.mode !== 'code' ? LESSON_MODE_LABEL[value.mode] : ''
|
* 改了做法时 `spellCode` 把「写代码」也写出来 —— 从画流程图改成写 C,只说「C语言」学生看不出不用画了
|
||||||
|
*/
|
||||||
|
function languageText(value: LessonUpdate, spellCode = false) {
|
||||||
|
const mode = value.mode && (spellCode || value.mode !== 'code') ? LESSON_MODE_LABEL[value.mode] : ''
|
||||||
return [mode, value.language ? LANGUAGE_SHOW_VALUE[value.language] : ''].filter(Boolean).join(' · ')
|
return [mode, value.language ? LANGUAGE_SHOW_VALUE[value.language] : ''].filter(Boolean).join(' · ')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +47,9 @@ const lines = computed(() => {
|
|||||||
if (value.added.length) result.push({ label: '加了', text: names(value.added) })
|
if (value.added.length) result.push({ label: '加了', text: names(value.added) })
|
||||||
if (value.removed.length) result.push({ label: '去掉了', text: names(value.removed) })
|
if (value.removed.length) result.push({ label: '去掉了', text: names(value.removed) })
|
||||||
if (value.languageChanged) {
|
if (value.languageChanged) {
|
||||||
const text = languageText(value)
|
// languageChanged 连做法一起算,分不出改的是哪样:一律写「做法」,连写代码带语言一起说
|
||||||
result.push({ label: value.mode && value.mode !== 'code' ? '做法' : '语言', text: text ? `改成 ${text}` : value.mode === 'code' ? '改成写代码' : '不限了' })
|
const text = languageText(value, true)
|
||||||
|
result.push({ label: '做法', text: text ? `改成 ${text}` : '不限了' })
|
||||||
}
|
}
|
||||||
if (value.reordered) result.push({ label: '', text: '换了题目的顺序' })
|
if (value.reordered) result.push({ label: '', text: '换了题目的顺序' })
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import { LESSON_MODE_LABEL } from '@oj2/contract'
|
import { LESSON_MODE_LABEL, type LessonMode } from '@oj2/contract'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { getClassActivity } from 'shared/api'
|
import { getClassActivity } from 'shared/api'
|
||||||
import { LANGUAGE_SHOW_VALUE } from 'utils/constants'
|
import { LANGUAGE_SHOW_VALUE } from 'utils/constants'
|
||||||
import type { ClassActivity, ClassActivityProblem } from 'utils/types'
|
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
|
const STALE_MS = 2 * 60 * 1000
|
||||||
|
|
||||||
@@ -89,8 +102,8 @@ export const useLessonStore = defineStore('lesson', () => {
|
|||||||
* 以前布置的)为空。写代码是默认的,不写出来
|
* 以前布置的)为空。写代码是默认的,不写出来
|
||||||
*/
|
*/
|
||||||
const languageText = computed(() => {
|
const languageText = computed(() => {
|
||||||
const mode = activity.value?.mode
|
const mode = activityMode(activity.value)
|
||||||
const language = activity.value?.language
|
const language = mode === 'flowchart' ? null : activity.value?.language
|
||||||
return [mode && mode !== 'code' ? LESSON_MODE_LABEL[mode] : '', language ? LANGUAGE_SHOW_VALUE[language] : ''].filter(Boolean).join(' · ')
|
return [mode && mode !== 'code' ? LESSON_MODE_LABEL[mode] : '', language ? LANGUAGE_SHOW_VALUE[language] : ''].filter(Boolean).join(' · ')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user