Compare commits

..
2 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5.5 773d15b033 feat(课堂看板): 「这节课的题」旁边多一个语言下拉,布置 C 的作业学生打开题目编辑器就是 C
Deploy / deploy (push) Canceled after 0s
默认是这个班上一次布置选的语言。学生那边首页、题号下拉、题目页课堂条都写上「C语言」;
打开这节课的题时编辑器自动切过去,每道题只切一次,学生自己再换就按他的。提交列表的
「这节课」连语言一起筛。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:54:21 +08:00
xuyueandClaude Opus 5.5 a18e129f4c feat(课堂): 布置这节课的题时可以选用什么语言做,学生用别的语言交对不算这份作业做完
class_lesson 加一列 language(0024,可空:以前布置的、按提交推断出来的课都不限语言)。
布置时有题不支持选的语言就整个拒掉并点名是哪几道;看板的格子、学生首页的做完状态、
提交列表「这节课」都只认这个语言的代码提交,流程图照旧算。看板多给 lastLanguage 当默认值。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:54:21 +08:00
13 changed files with 4311 additions and 33 deletions

No files matched your search

@@ -0,0 +1 @@
ALTER TABLE "class_lesson" ADD COLUMN "language" text;
File diff suppressed because it is too large. Load diff
+7
View File
@@ -169,6 +169,13 @@
"when": 1790519287984, "when": 1790519287984,
"tag": "0023_add_class_lesson", "tag": "0023_add_class_lesson",
"breakpoints": true "breakpoints": true
},
{
"idx": 24,
"version": "7",
"when": 1790732755406,
"tag": "0024_add_class_lesson_language",
"breakpoints": true
} }
] ]
} }
+6
View File
@@ -50,6 +50,7 @@ import type {
ProblemSetDifficulty, ProblemSetDifficulty,
ProblemSetStatus, ProblemSetStatus,
ReactionKey, ReactionKey,
RunnableLanguage,
SqlConfig, SqlConfig,
SqlDisplay, SqlDisplay,
TagCategory, TagCategory,
@@ -1477,6 +1478,11 @@ export const classLesson = pgTable(
className: text("class_name").notNull(), className: text("class_name").notNull(),
day: text().notNull(), day: text().notNull(),
problemIds: jsonb("problem_ids").notNull().$type<number[]>(), problemIds: jsonb("problem_ids").notNull().$type<number[]>(),
/**
* 这份作业用什么语言做(Python / C / C++),老师布置时选。可空:加这一列之前布置的
* 没有它。写入侧在 PUT /classroom/lesson 按 runnableLanguageSchema 校验过
*/
language: text().$type<RunnableLanguage>(),
createdBy: integer("created_by").notNull(), createdBy: integer("created_by").notNull(),
updatedAt: timestamp("updated_at", { updatedAt: timestamp("updated_at", {
withTimezone: true, withTimezone: true,
+71 -16
View File
@@ -12,6 +12,7 @@ import {
type ClassRankItem, type ClassRankItem,
type ClassUserRank, type ClassUserRank,
type LastVisit, type LastVisit,
type RunnableLanguage,
FLOWCHART_PASS_GRADES, FLOWCHART_PASS_GRADES,
} from "@oj2/contract" } from "@oj2/contract"
import { import {
@@ -224,8 +225,10 @@ function activityRows(options: {
since: string since: string
users?: number[] | ReturnType<typeof classMemberIds> users?: number[] | ReturnType<typeof classMemberIds>
problemIds?: number[] problemIds?: number[]
/** 老师布置时选了语言:代码只数这个语言交的,流程图不受影响 */
language?: RunnableLanguage | null
}) { }) {
const { since, users, problemIds } = options const { since, users, problemIds, language } = options
const code = db const code = db
.select({ .select({
userId: schema.submission.userId, userId: schema.submission.userId,
@@ -240,6 +243,7 @@ function activityRows(options: {
gte(schema.submission.createTime, since), gte(schema.submission.createTime, since),
users ? inArray(schema.submission.userId, users) : undefined, users ? inArray(schema.submission.userId, users) : undefined,
problemIds ? inArray(schema.submission.problemId, problemIds) : undefined, problemIds ? inArray(schema.submission.problemId, problemIds) : undefined,
language ? eq(schema.submission.language, language) : undefined,
), ),
) )
const flowchart = db const flowchart = db
@@ -268,12 +272,13 @@ function activityRows(options: {
async function classDayGroups( async function classDayGroups(
className: string, className: string,
since: string, since: string,
options: { minUsers?: number; problemIds?: number[] } = {}, options: { minUsers?: number; problemIds?: number[]; language?: RunnableLanguage | null } = {},
): Promise<ClassDayGroup[]> { ): Promise<ClassDayGroup[]> {
const activity = activityRows({ const activity = activityRows({
since, since,
users: classMemberIds(className), users: classMemberIds(className),
problemIds: options.problemIds, problemIds: options.problemIds,
language: options.language,
}) })
const day = sql<string>`to_char(${localTime(activity.createTime)}, 'YYYY-MM-DD')` const day = sql<string>`to_char(${localTime(activity.createTime)}, 'YYYY-MM-DD')`
const userCount = sql<number>`count(distinct ${activity.userId})::int` const userCount = sql<number>`count(distinct ${activity.userId})::int`
@@ -310,14 +315,14 @@ async function visibleProblems(ids: number[]) {
return new Map(rows.map((row) => [row.id, row])) return new Map(rows.map((row) => [row.id, row]))
} }
/** 老师给这个班今天布置的题(problem.id,按输入顺序);没布置为 null */ /** 老师给这个班今天布置的题(problem.id,按输入顺序)和选的语言;没布置为 null */
async function lessonProblemIds(className: string, day: string) { async function lessonPlan(className: string, day: string) {
const [row] = await db const [row] = await db
.select({ problemIds: schema.classLesson.problemIds }) .select({ problemIds: schema.classLesson.problemIds, language: schema.classLesson.language })
.from(schema.classLesson) .from(schema.classLesson)
.where(and(eq(schema.classLesson.className, className), eq(schema.classLesson.day, day))) .where(and(eq(schema.classLesson.className, className), eq(schema.classLesson.day, day)))
.limit(1) .limit(1)
return row?.problemIds.length ? row.problemIds : null return row?.problemIds.length ? { problemIds: row.problemIds, language: row.language } : null
} }
/** /**
@@ -330,16 +335,18 @@ async function lessonProblemIds(className: string, day: string) {
*/ */
async function classLessonProblems(className: string, lookbackDays: number) { async function classLessonProblems(className: string, lookbackDays: number) {
const today = calendarDay() const today = calendarDay()
const planned = await lessonProblemIds(className, today) const plan = await lessonPlan(className, today)
if (planned) { if (plan) {
const planned = plan.problemIds
const [problems, groups] = await Promise.all([ const [problems, groups] = await Promise.all([
visibleProblems(planned), visibleProblems(planned),
classDayGroups(className, dayStart(), { problemIds: planned }), classDayGroups(className, dayStart(), { problemIds: planned, language: plan.language }),
]) ])
const groupById = new Map(groups.map((row) => [row.problemId, row])) const groupById = new Map(groups.map((row) => [row.problemId, row]))
return { return {
source: "teacher" as const, source: "teacher" as const,
day: today, day: today,
language: plan.language ?? null,
problems: planned.flatMap((id) => { problems: planned.flatMap((id) => {
const problem = problems.get(id) const problem = problems.get(id)
if (!problem) return [] if (!problem) return []
@@ -369,6 +376,8 @@ async function classLessonProblems(className: string, lookbackDays: number) {
return { return {
source: picked.length ? ("inferred" as const) : null, source: picked.length ? ("inferred" as const) : null,
day: picked.length ? latest : null, day: picked.length ? latest : null,
// 推断出来的课不知道老师要的是哪种语言,不限
language: null,
problems: picked.flatMap((row) => { problems: picked.flatMap((row) => {
const problem = problems.get(row.problemId) const problem = problems.get(row.problemId)
return problem return problem
@@ -502,6 +511,7 @@ classroomRoutes.get("/me/class-activity", requireAuth, async (c) => {
className: null, className: null,
day: null, day: null,
source: null, source: null,
language: null,
problems: [], problems: [],
} satisfies ClassActivity) } satisfies ClassActivity)
} }
@@ -522,6 +532,8 @@ classroomRoutes.get("/me/class-activity", requireAuth, async (c) => {
eq(schema.submission.userId, user.id), eq(schema.submission.userId, user.id),
inArray(schema.submission.problemId, ids), inArray(schema.submission.problemId, ids),
isNull(schema.submission.contestId), isNull(schema.submission.contestId),
// 布置的是 C:以前用 Python 做对过不算这份作业做完
lesson.language ? eq(schema.submission.language, lesson.language) : undefined,
), ),
) )
.groupBy(schema.submission.problemId) .groupBy(schema.submission.problemId)
@@ -554,6 +566,7 @@ classroomRoutes.get("/me/class-activity", requireAuth, async (c) => {
className: user.className, className: user.className,
day: lesson.day, day: lesson.day,
source: lesson.source, source: lesson.source,
language: lesson.language,
problems: lesson.problems.map((problem) => { problems: lesson.problems.map((problem) => {
const accepted = mineById.get(problem.id) const accepted = mineById.get(problem.id)
return { return {
@@ -619,14 +632,27 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
className: null, className: null,
day, day,
source: null, source: null,
language: null,
lastLanguage: null,
problems: [], problems: [],
students: [], students: [],
recentLessons: 0, recentLessons: 0,
} satisfies ClassBoard) } satisfies ClassBoard)
} }
// 推断只看今天(回看 1 天):看板是给这节课用的,昨天的题不该冒出来 // 推断只看今天(回看 1 天):看板是给这节课用的,昨天的题不该冒出来。
const lesson = await classLessonProblems(className, 1) // lastLanguage 给语言下拉当默认值:这个班上一次布置选的什么(一个班一般一学期只学一种)
const [lesson, [last]] = await Promise.all([
classLessonProblems(className, 1),
db
.select({ language: schema.classLesson.language })
.from(schema.classLesson)
.where(
and(eq(schema.classLesson.className, className), isNotNull(schema.classLesson.language)),
)
.orderBy(desc(schema.classLesson.day))
.limit(1),
])
const ids = lesson.problems.map((problem) => problem.id) const ids = lesson.problems.map((problem) => problem.id)
const start = dayStart() const start = dayStart()
@@ -680,6 +706,7 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
isNull(schema.submission.contestId), isNull(schema.submission.contestId),
inArray(schema.submission.userId, userIds), inArray(schema.submission.userId, userIds),
inArray(schema.submission.problemId, ids), inArray(schema.submission.problemId, ids),
lesson.language ? eq(schema.submission.language, lesson.language) : undefined,
), ),
) )
.groupBy(schema.submission.userId, schema.submission.problemId) .groupBy(schema.submission.userId, schema.submission.problemId)
@@ -774,6 +801,8 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
className, className,
day, day,
source: lesson.source, source: lesson.source,
language: lesson.language,
lastLanguage: last?.language ?? null,
problems: lesson.problems.map((problem) => ({ problems: lesson.problems.map((problem) => ({
problemId: problem.id, problemId: problem.id,
problemDisplayId: problem.displayId, problemDisplayId: problem.displayId,
@@ -815,12 +844,18 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
classroomRoutes.get("/classroom/lesson", requireTeacher, async (c) => { classroomRoutes.get("/classroom/lesson", requireTeacher, async (c) => {
const className = c.req.query("className")?.trim() || (await suggestActiveClass()) const className = c.req.query("className")?.trim() || (await suggestActiveClass())
if (!className) if (!className)
return success(c, { className: null, source: null, problems: [] } satisfies ClassLesson) return success(c, {
className: null,
source: null,
language: null,
problems: [],
} satisfies ClassLesson)
// 和看板一样只回看今天:「这节课」不该冒出昨天的题 // 和看板一样只回看今天:「这节课」不该冒出昨天的题
const lesson = await classLessonProblems(className, 1) const lesson = await classLessonProblems(className, 1)
return success(c, { return success(c, {
className, className,
source: lesson.source, source: lesson.source,
language: lesson.language,
problems: lesson.problems.map((problem) => ({ problems: lesson.problems.map((problem) => ({
problemDisplayId: problem.displayId, problemDisplayId: problem.displayId,
title: problem.title, title: problem.title,
@@ -834,10 +869,11 @@ classroomRoutes.get("/classroom/lesson", requireTeacher, async (c) => {
* 学生那边退回推断。 * 学生那边退回推断。
*/ */
classroomRoutes.put("/classroom/lesson", requireTeacher, async (c) => { classroomRoutes.put("/classroom/lesson", requireTeacher, async (c) => {
const parsed = await parseBody(c, classLessonRequestSchema, "题号格式不对") const parsed = await parseBody(c, classLessonRequestSchema, "题号或语言不对")
if (!parsed.success) return parsed.response if (!parsed.success) return parsed.response
const user = c.get("user")! const user = c.get("user")!
const { className, problemDisplayIds } = parsed.data const { className, problemDisplayIds } = parsed.data
const language = parsed.data.language ?? null
const day = calendarDay() const day = calendarDay()
const [exists] = await db const [exists] = await db
@@ -856,7 +892,11 @@ classroomRoutes.put("/classroom/lesson", requireTeacher, async (c) => {
const wanted = [...new Set(problemDisplayIds.map((id) => id.toLowerCase()))] const wanted = [...new Set(problemDisplayIds.map((id) => id.toLowerCase()))]
const found = await db const found = await db
.select({ id: schema.problem.id, displayId: schema.problem.displayId }) .select({
id: schema.problem.id,
displayId: schema.problem.displayId,
languages: schema.problem.languages,
})
.from(schema.problem) .from(schema.problem)
.where( .where(
and( and(
@@ -871,14 +911,29 @@ classroomRoutes.put("/classroom/lesson", requireTeacher, async (c) => {
return failure(c, 400, "problem-not-found", `这些题号不存在或没有公开:${missing.join("、")}`) return failure(c, 400, "problem-not-found", `这些题号不存在或没有公开:${missing.join("、")}`)
} }
// 选了语言就得每道都能用它交,不然学生点开才发现交不了(提交接口会拒:language-not-allowed)
if (language) {
const unsupported = found
.filter((row) => !row.languages.includes(language))
.map((row) => row.displayId)
if (unsupported.length) {
return failure(
c,
400,
"language-not-allowed",
`这些题不能用 ${language} 交:${unsupported.join("、")}`,
)
}
}
const problemIds = wanted.map((id) => idByDisplay.get(id)!) const problemIds = wanted.map((id) => idByDisplay.get(id)!)
const now = new Date().toISOString() const now = new Date().toISOString()
await db await db
.insert(schema.classLesson) .insert(schema.classLesson)
.values({ className, day, problemIds, createdBy: user.id, updatedAt: now }) .values({ className, day, problemIds, language, createdBy: user.id, updatedAt: now })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: [schema.classLesson.className, schema.classLesson.day], target: [schema.classLesson.className, schema.classLesson.day],
set: { problemIds, createdBy: user.id, updatedAt: now }, set: { problemIds, language, createdBy: user.id, updatedAt: now },
}) })
return success(c, null) return success(c, null)
}) })
+7 -2
View File
@@ -41,6 +41,7 @@ import {
type FlowchartStatistics, type FlowchartStatistics,
type SubmissionStatistics, type SubmissionStatistics,
type SubmissionLessons, type SubmissionLessons,
type RunnableLanguage,
type SubmissionStatisticsGrid, type SubmissionStatisticsGrid,
type TodaySubmissionStatistics, type TodaySubmissionStatistics,
} from "@oj2/contract" } from "@oj2/contract"
@@ -242,8 +243,12 @@ export function getClassLesson(className?: string) {
} }
/** 给这个班布置今天的题,空数组 = 清掉 */ /** 给这个班布置今天的题,空数组 = 清掉 */
export function setClassLesson(className: string, problemDisplayIds: string[]) { export function setClassLesson(
return api.put<null>("classroom/lesson", { className, problemDisplayIds }) 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 { useHiddenStudents } from "shared/composables/hiddenStudents"
import { useCollabStore } from "shared/store/collab" import { useCollabStore } from "shared/store/collab"
import { useConfigStore } from "shared/store/config" import { useConfigStore } from "shared/store/config"
import type { RunnableLanguage } from "@oj2/contract"
import { errorMessage } from "utils/api" import { errorMessage } from "utils/api"
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import type { ClassBoard, ClassBoardCell, ClassBoardStudent } from "utils/types" import type { ClassBoard, ClassBoardCell, ClassBoardStudent } from "utils/types"
@@ -34,6 +36,16 @@ const updatedAt = ref<Date | null>(null)
const projector = ref(false) const projector = ref(false)
const lessonInput = ref("") 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) const saving = ref(false)
function classLabel(name: string) { function classLabel(name: string) {
@@ -65,7 +77,7 @@ async function load() {
} }
} }
// 换了班要把输入框换成那个班今天布置的题 // 换了班要把输入框换成那个班今天布置的题、布置时选的语言(15 秒一次的刷新不动它们)
watch( watch(
() => [board.value?.className, board.value?.source] as const, () => [board.value?.className, board.value?.source] as const,
() => { () => {
@@ -73,6 +85,7 @@ watch(
board.value?.source === "teacher" board.value?.source === "teacher"
? board.value.problems.map((problem) => problem.problemDisplayId).join(" ") ? 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) const ids = lessonInput.value.split(/[\s,,、;;]+/).filter(Boolean)
saving.value = true saving.value = true
try { try {
await setClassLesson(className.value, ids) await setClassLesson(className.value, ids, ids.length ? lessonLanguage.value : null)
message.success(ids.length ? "已布置,学生首页会显示这几道题" : "已清掉,学生那边改回自动推断") message.success(
ids.length
? `已布置(${LANGUAGE_SHOW_VALUE[lessonLanguage.value]}),学生首页会显示这几道题`
: "已清掉,学生那边改回自动推断",
)
await load() await load()
} catch (error) { } catch (error) {
message.error(errorMessage(error)) message.error(errorMessage(error))
@@ -241,7 +258,12 @@ function submissionsHref(
username: student.username, username: student.username,
exactUsername: "1", exactUsername: "1",
today: "1", today: "1",
...(flowchart ? { language: "Flowchart" } : {}), // 布置时选了语言的,代码那边只看这个语言交的(和格子里数的一致)
...(flowchart
? { language: "Flowchart" }
: board.value?.language
? { language: board.value.language }
: {}),
...(problemDisplayId ? { problem: problemDisplayId } : {}), ...(problemDisplayId ? { problem: problemDisplayId } : {}),
}, },
}).href }).href
@@ -293,6 +315,12 @@ function submissionsHref(
<n-card v-if="!projector" size="small" class="section"> <n-card v-if="!projector" size="small" class="section">
<n-flex align="center" :wrap="false"> <n-flex align="center" :wrap="false">
<n-text strong style="flex-shrink: 0">这节课的题</n-text> <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 <n-input
v-model:value="lessonInput" v-model:value="lessonInput"
placeholder="输入题号,用空格隔开,比如 8019 8020 8021" placeholder="输入题号,用空格隔开,比如 8019 8020 8021"
@@ -303,13 +331,15 @@ function submissionsHref(
</n-flex> </n-flex>
<n-text depth="3" class="meta"> <n-text depth="3" class="meta">
<template v-if="board.source === 'teacher'"> <template v-if="board.source === 'teacher'">
学生首页的「班里在做」显示的就是这几道。清空再点布置就改回自动推断。 学生首页的「班里在做」显示的就是这几道<template v-if="board.language"
>,用 {{ LANGUAGE_SHOW_VALUE[board.language] }} 交对才算做完</template
>。清空再点布置就改回自动推断。
</template> </template>
<template v-else-if="board.source === 'inferred'"> <template v-else-if="board.source === 'inferred'">
还没布置。下面这几道是按今天的提交记录推断的(同班 5 人以上做过的题)。 还没布置。下面这几道是按今天的提交记录推断的(同班 5 人以上做过的题)。
</template> </template>
<template v-else> <template v-else>
布置之后,学生打开首页就能看到这几道题,没听清题号的也能找到。 布置之后,学生打开首页就能看到这几道题,没听清题号的也能找到;打开题目时编辑器默认就是左边选的语言。
</template> </template>
</n-text> </n-text>
</n-card> </n-card>
+5 -2
View File
@@ -17,7 +17,7 @@ import { useBreakpoints } from "shared/composables/breakpoints"
import { useProblemJump } from "shared/composables/problemJump" import { useProblemJump } from "shared/composables/problemJump"
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 { 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 { duration, parseTime, zonedParts, zonedYear } from "utils/functions"
import type { import type {
AnnouncementListItem, AnnouncementListItem,
@@ -227,7 +227,10 @@ const classActivityTitle = computed(() => {
const day = classActivity.value?.day const day = classActivity.value?.day
if (!day) return "" 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 "今天班里在做" if (classToday.value) return "今天班里在做"
const [, month, date] = day.split("-").map(Number) const [, month, date] = day.split("-").map(Number)
return `${month}月${date}日班里做了` return `${month}月${date}日班里做了`
@@ -232,7 +232,10 @@ onSolvedHere(() => {
<template> <template>
<!-- 课堂 --> <!-- 课堂 -->
<div v-if="lesson" class="context-bar lesson"> <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> <span v-if="isMobile" class="meta">这是第 {{ lesson.position }} 道</span>
<ProblemChips v-else :items="lesson.chips" :current="currentId" variant="lesson" /> <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 { useProblemPageContext } from "../composables/problemPageContext"
import CollabBar from "./CollabBar.vue" import CollabBar from "./CollabBar.vue"
import { useFlowchartStore } from "oj/store/flowchart" import { useFlowchartStore } from "oj/store/flowchart"
import { useLessonStore } from "oj/store/lesson"
const FlowchartEditor = defineAsyncComponent( const FlowchartEditor = defineAsyncComponent(
() => import("shared/components/FlowchartEditor/index.vue"), () => 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 藏起来, * 画着流程图时忽然不让画了:工具栏的「写代码 / 画流程图」和语言下拉都跟着 canDraw 藏起来,
* 编辑器要是还停在画布上,学生就回不去写代码了。拖窄到手机宽度时页面会换一套布局、编辑器 * 编辑器要是还停在画布上,学生就回不去写代码了。拖窄到手机宽度时页面会换一套布局、编辑器
+12 -3
View File
@@ -1,5 +1,6 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { getClassActivity } from "oj/api" import { getClassActivity } from "oj/api"
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import type { ClassActivity, ClassActivityProblem } from "utils/types" 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 } 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(() => 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) { if (lessonActive.value) {
query.problem = "" query.problem = ""
query.today = "0" query.today = "0"
// 「这节课」顺带筛上的语言一起放掉;老师自己另选的别动
if (lesson.value?.language && query.language === lesson.value.language) query.language = ""
return return
} }
lessonLoading.value = true lessonLoading.value = true
@@ -525,6 +527,8 @@ async function toggleLesson() {
lesson.value = res lesson.value = res
query.problem = lessonIds.value query.problem = lessonIds.value
query.today = "1" query.today = "1"
// 老师布置时选了语言:看板上只数这个语言交的,列表也只列它(流程图那边不动)
if (res.language && !flowMode.value) query.language = res.language
} finally { } finally {
lessonLoading.value = false lessonLoading.value = false
} }
+27 -3
View File
@@ -1,6 +1,7 @@
import { z } from "zod" import { z } from "zod"
import { judgeStatusSchema } from "./judge-status" import { judgeStatusSchema } from "./judge-status"
import { runnableLanguageSchema } from "./language"
export const classRankItemSchema = z.object({ export const classRankItemSchema = z.object({
className: z.string(), className: z.string(),
@@ -76,15 +77,25 @@ export const classActivityProblemSchema = z.object({
userCount: z.number().int(), userCount: z.number().int(),
/** 其中那天通过了的人数 */ /** 其中那天通过了的人数 */
acceptedCount: z.number().int(), acceptedCount: z.number().int(),
/** 我自己在题库里(不含比赛)做这道题的状态,不限那一天 */ /**
* 我自己在题库里(不含比赛)做这道题的状态,不限那一天。老师布置时选了语言的,
* 代码只认用那个语言交的(流程图照旧算)
*/
myStatus: z.enum(["accepted", "tried", "none"]), myStatus: z.enum(["accepted", "tried", "none"]),
}) })
/**
* 老师布置这节课时选的语言(Python / C / C++)。null = 没选:推断出来的课、加这一项之前
* 布置的课。选了之后这份作业「做完」只认这个语言交对的,学生打开这几道题编辑器默认就是它
*/
const lessonLanguageSchema = runnableLanguageSchema.nullable()
export const classActivitySchema = z.object({ export const classActivitySchema = z.object({
className: z.string().nullable(), className: z.string().nullable(),
day: z.string().nullable(), day: z.string().nullable(),
/** teacher = 老师在课堂看板里布置的;inferred = 从同班提交记录推断的 */ /** teacher = 老师在课堂看板里布置的;inferred = 从同班提交记录推断的 */
source: z.enum(["teacher", "inferred"]).nullable(), source: z.enum(["teacher", "inferred"]).nullable(),
language: lessonLanguageSchema,
problems: z.array(classActivityProblemSchema), problems: z.array(classActivityProblemSchema),
}) })
@@ -122,10 +133,14 @@ export const lastVisitSchema = z.object({
.nullable(), .nullable(),
}) })
/** 课堂看板里老师布置这节课的题:按输入顺序给展示题号,最多 20 道,空数组 = 清掉 */ /**
* 课堂看板里老师布置这节课的题:按输入顺序给展示题号,最多 20 道,空数组 = 清掉。
* `language` 是这份作业用什么语言做;不传当 null(上线时还开着的旧看板页面不带它)
*/
export const classLessonRequestSchema = z.object({ export const classLessonRequestSchema = z.object({
className: z.string().trim().min(1), className: z.string().trim().min(1),
problemDisplayIds: z.array(z.string().trim().min(1)).max(20), problemDisplayIds: z.array(z.string().trim().min(1)).max(20),
language: lessonLanguageSchema.optional(),
}) })
export const classBoardProblemSchema = z.object({ export const classBoardProblemSchema = z.object({
@@ -135,7 +150,10 @@ export const classBoardProblemSchema = z.object({
}) })
export const classBoardCellSchema = z.object({ export const classBoardCellSchema = z.object({
/** accepted 看的是题库里(不含比赛)有没有通过过,不限今天 —— 以前做过的也算做完 */ /**
* accepted 看的是题库里(不含比赛)有没有通过过,不限今天 —— 以前做过的也算做完。
* 布置时选了语言的,代码的次数和通过都只数那个语言(流程图照旧)
*/
status: z.enum(["accepted", "tried", "none"]), status: z.enum(["accepted", "tried", "none"]),
/** 今天在这道题上交了几次(代码 + 流程图) */ /** 今天在这道题上交了几次(代码 + 流程图) */
attempts: z.number().int(), attempts: z.number().int(),
@@ -175,6 +193,10 @@ export const classBoardSchema = z.object({
className: z.string().nullable(), className: z.string().nullable(),
day: z.string(), day: z.string(),
source: z.enum(["teacher", "inferred"]).nullable(), source: z.enum(["teacher", "inferred"]).nullable(),
/** 今天布置的语言(见 lessonLanguageSchema) */
language: lessonLanguageSchema,
/** 这个班最近一次布置选的语言(含今天),给语言下拉当默认值;从没选过为 null */
lastLanguage: lessonLanguageSchema,
problems: z.array(classBoardProblemSchema), problems: z.array(classBoardProblemSchema),
students: z.array(classBoardStudentSchema), students: z.array(classBoardStudentSchema),
/** /**
@@ -192,6 +214,8 @@ export const classBoardSchema = z.object({
export const classLessonSchema = z.object({ export const classLessonSchema = z.object({
className: z.string().nullable(), className: z.string().nullable(),
source: z.enum(["teacher", "inferred"]).nullable(), source: z.enum(["teacher", "inferred"]).nullable(),
/** 选了语言时,列表那边连语言一起筛 */
language: lessonLanguageSchema,
problems: z.array(z.object({ problemDisplayId: z.string(), title: z.string() })), problems: z.array(z.object({ problemDisplayId: z.string(), title: z.string() })),
}) })