feat(课堂看板): 「这节课的题」旁边多一个语言下拉,布置 C 的作业学生打开题目编辑器就是 C
Deploy / deploy (push) Canceled after 0s

默认是这个班上一次布置选的语言。学生那边首页、题号下拉、题目页课堂条都写上「C语言」;
打开这节课的题时编辑器自动切过去,每道题只切一次,学生自己再换就按他的。提交列表的
「这节课」连语言一起筛。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-30 09:54:21 +08:00
1 parent a18e129f4c
commit 773d15b033
7 files changed
+93 -14

No files matched your search

+7 -2
View File
@@ -41,6 +41,7 @@ import {
type FlowchartStatistics,
type SubmissionStatistics,
type SubmissionLessons,
type RunnableLanguage,
type SubmissionStatisticsGrid,
type TodaySubmissionStatistics,
} from "@oj2/contract"
@@ -242,8 +243,12 @@ export function getClassLesson(className?: string) {
}
/** 给这个班布置今天的题,空数组 = 清掉 */
export function setClassLesson(className: string, problemDisplayIds: string[]) {
return api.put<null>("classroom/lesson", { className, problemDisplayIds })
export function setClassLesson(
className: string,
problemDisplayIds: string[],
language: RunnableLanguage | null,
) {
return api.put<null>("classroom/lesson", { className, problemDisplayIds, language })
}
/** 我的知识点地图(只有自己的,不能查别人) */
+36 -6
View File
@@ -3,7 +3,9 @@ import { getClassBoard, setClassLesson } from "oj/api"
import { useHiddenStudents } from "shared/composables/hiddenStudents"
import { useCollabStore } from "shared/store/collab"
import { useConfigStore } from "shared/store/config"
import type { RunnableLanguage } from "@oj2/contract"
import { errorMessage } from "utils/api"
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import { parseTime } from "utils/functions"
import type { ClassBoard, ClassBoardCell, ClassBoardStudent } from "utils/types"
@@ -34,6 +36,16 @@ const updatedAt = ref<Date | null>(null)
const projector = ref(false)
const lessonInput = ref("")
/**
* 这份作业用什么语言做,跟着这一次布置走(用户定的:不是给班级定死的设置)。默认是这个班
* 上一次布置时选的;选了之后看板和学生那边的「做完」都只认这个语言交对的
*/
const lessonLanguage = ref<RunnableLanguage>("Python")
const LESSON_LANGUAGES: RunnableLanguage[] = ["Python", "C", "C++"]
const languageOptions = LESSON_LANGUAGES.map((value) => ({
label: LANGUAGE_SHOW_VALUE[value],
value,
}))
const saving = ref(false)
function classLabel(name: string) {
@@ -65,7 +77,7 @@ async function load() {
}
}
// 换了班要把输入框换成那个班今天布置的题
// 换了班要把输入框换成那个班今天布置的题、布置时选的语言(15 秒一次的刷新不动它们)
watch(
() => [board.value?.className, board.value?.source] as const,
() => {
@@ -73,6 +85,7 @@ watch(
board.value?.source === "teacher"
? board.value.problems.map((problem) => problem.problemDisplayId).join(" ")
: ""
lessonLanguage.value = board.value?.language ?? board.value?.lastLanguage ?? "Python"
},
)
@@ -86,8 +99,12 @@ async function saveLesson() {
const ids = lessonInput.value.split(/[\s,,、;;]+/).filter(Boolean)
saving.value = true
try {
await setClassLesson(className.value, ids)
message.success(ids.length ? "已布置,学生首页会显示这几道题" : "已清掉,学生那边改回自动推断")
await setClassLesson(className.value, ids, ids.length ? lessonLanguage.value : null)
message.success(
ids.length
? `已布置(${LANGUAGE_SHOW_VALUE[lessonLanguage.value]}),学生首页会显示这几道题`
: "已清掉,学生那边改回自动推断",
)
await load()
} catch (error) {
message.error(errorMessage(error))
@@ -241,7 +258,12 @@ function submissionsHref(
username: student.username,
exactUsername: "1",
today: "1",
...(flowchart ? { language: "Flowchart" } : {}),
// 布置时选了语言的,代码那边只看这个语言交的(和格子里数的一致)
...(flowchart
? { language: "Flowchart" }
: board.value?.language
? { language: board.value.language }
: {}),
...(problemDisplayId ? { problem: problemDisplayId } : {}),
},
}).href
@@ -293,6 +315,12 @@ function submissionsHref(
<n-card v-if="!projector" size="small" class="section">
<n-flex align="center" :wrap="false">
<n-text strong style="flex-shrink: 0">这节课的题</n-text>
<n-select
v-model:value="lessonLanguage"
:options="languageOptions"
aria-label="用什么语言做"
style="width: 110px; flex-shrink: 0"
/>
<n-input
v-model:value="lessonInput"
placeholder="输入题号,用空格隔开,比如 8019 8020 8021"
@@ -303,13 +331,15 @@ function submissionsHref(
</n-flex>
<n-text depth="3" class="meta">
<template v-if="board.source === 'teacher'">
学生首页的「班里在做」显示的就是这几道。清空再点布置就改回自动推断。
学生首页的「班里在做」显示的就是这几道<template v-if="board.language"
>,用 {{ LANGUAGE_SHOW_VALUE[board.language] }} 交对才算做完</template
>。清空再点布置就改回自动推断。
</template>
<template v-else-if="board.source === 'inferred'">
还没布置。下面这几道是按今天的提交记录推断的(同班 5 人以上做过的题)。
</template>
<template v-else>
布置之后,学生打开首页就能看到这几道题,没听清题号的也能找到。
布置之后,学生打开首页就能看到这几道题,没听清题号的也能找到;打开题目时编辑器默认就是左边选的语言。
</template>
</n-text>
</n-card>
+5 -2
View File
@@ -17,7 +17,7 @@ import { useBreakpoints } from "shared/composables/breakpoints"
import { useProblemJump } from "shared/composables/problemJump"
import { pickHeadline, type Headline } from "oj/user/knowledge"
import { useUserStore } from "shared/store/user"
import { ContestStatus, CONTEST_STATUS, JUDGE_STATUS } from "utils/constants"
import { ContestStatus, CONTEST_STATUS, JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
import { duration, parseTime, zonedParts, zonedYear } from "utils/functions"
import type {
AnnouncementListItem,
@@ -227,7 +227,10 @@ const classActivityTitle = computed(() => {
const day = classActivity.value?.day
if (!day) return ""
// 老师在课堂看板布置的,说成「老师布置的」,比「班里在做」更让人知道该做这几道
if (classActivity.value?.source === "teacher") return "今天老师布置的题"
if (classActivity.value?.source === "teacher") {
const language = classActivity.value.language
return `今天老师布置的题${language ? ` · 用${LANGUAGE_SHOW_VALUE[language]}做` : ""}`
}
if (classToday.value) return "今天班里在做"
const [, month, date] = day.split("-").map(Number)
return `${month}月${date}日班里做了`
@@ -232,7 +232,10 @@ onSolvedHere(() => {
<template>
<!-- 课堂 -->
<div v-if="lesson" class="context-bar lesson">
<span class="lead lesson-lead">这节课 做完 {{ lesson.done }}/{{ lesson.total }}</span>
<span class="lead lesson-lead"
>这节课<template v-if="lessonStore.languageText"> · {{ lessonStore.languageText }}</template>
做完 {{ lesson.done }}/{{ lesson.total }}</span
>
<!-- 手机上放不下一排题号:只说是第几道,箭头做大(设计稿「手机:题目 / 代码 / 结果」) -->
<span v-if="isMobile" class="meta">这是第 {{ lesson.position }} 道</span>
<ProblemChips v-else :items="lesson.chips" :current="currentId" variant="lesson" />
@@ -16,6 +16,7 @@ import { useCollabHere, useTeacherCollab } from "../composables/teacherCollab"
import { useProblemPageContext } from "../composables/problemPageContext"
import CollabBar from "./CollabBar.vue"
import { useFlowchartStore } from "oj/store/flowchart"
import { useLessonStore } from "oj/store/lesson"
const FlowchartEditor = defineAsyncComponent(
() => import("shared/components/FlowchartEditor/index.vue"),
@@ -151,6 +152,30 @@ watch(
},
)
/**
* 老师在课堂看板布置这节课时选了语言:打开这节课的题,编辑器默认就用它。学 C 的班,
* 编辑器停在默认的 Python(或者上一个用这台电脑的人留下的偏好)的话,学生交上去才发现
* 不算数。课堂条只在题库入口出现,这里同一个范围。
*
* 每道题只自动换一次:学生自己再切回别的语言,就按他的来。课堂状态可能比题面晚到
* (课堂条进页面才去拉),所以两边都等到了再判断;画着流程图时不动他。
*/
const lessonStore = useLessonStore()
let lessonLanguageFor: string | null = null
watch(
() => [problem.value?._id, lessonStore.activity] as const,
([id, activity]) => {
if (!id || !activity || lessonLanguageFor === id) return
lessonLanguageFor = id
const language = activity.language
if (!language || ctx.value.entry !== "problem" || teacherCollab.value) return
if (!lessonStore.includes(id) || !problemStore.codeLanguages.includes(language)) return
if (codeStore.code.language === language || codeStore.code.language === "Flowchart") return
problemStore.switchLanguage(language)
},
{ immediate: true },
)
/**
* 画着流程图时忽然不让画了:工具栏的「写代码 / 画流程图」和语言下拉都跟着 canDraw 藏起来,
* 编辑器要是还停在画布上,学生就回不去写代码了。拖窄到手机宽度时页面会换一套布局、编辑器
+12 -3
View File
@@ -1,5 +1,6 @@
import { defineStore } from "pinia"
import { getClassActivity } from "oj/api"
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import type { ClassActivity, ClassActivityProblem } from "utils/types"
/** 同一节课里来回换题不用每次都重拉;过了这么久再进题目页就重拉一次 */
@@ -74,10 +75,18 @@ export const useLessonStore = defineStore("lesson", () => {
return { total: list.length, next: rest[0] ?? null, remaining: rest.length }
}
/** 老师布置的叫「老师布置的题」,推断出来的叫「这节课的题」 */
/** 老师布置时选的语言,给界面写「C语言」;没选(推断出来的课、以前布置的)为空 */
const languageText = computed(() => {
const language = activity.value?.language
return language ? LANGUAGE_SHOW_VALUE[language] : ""
})
/** 老师布置的叫「老师布置的题」(选了语言带上语言),推断出来的叫「这节课的题」 */
const label = computed(() =>
activity.value?.source === "teacher" ? "老师布置的题" : "这节课的题",
activity.value?.source === "teacher"
? `老师布置的题${languageText.value ? ` · ${languageText.value}` : ""}`
: "这节课的题",
)
return { activity, label, load, markAccepted, includes, nextAfter }
return { activity, label, languageText, load, markAccepted, includes, nextAfter }
})
+4
View File
@@ -508,6 +508,8 @@ async function toggleLesson() {
if (lessonActive.value) {
query.problem = ""
query.today = "0"
// 「这节课」顺带筛上的语言一起放掉;老师自己另选的别动
if (lesson.value?.language && query.language === lesson.value.language) query.language = ""
return
}
lessonLoading.value = true
@@ -525,6 +527,8 @@ async function toggleLesson() {
lesson.value = res
query.problem = lessonIds.value
query.today = "1"
// 老师布置时选了语言:看板上只数这个语言交的,列表也只列它(流程图那边不动)
if (res.language && !flowMode.value) query.language = res.language
} finally {
lessonLoading.value = false
}