Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e4abbc1645
commit
3ca3b42997
5 files changed
+36
-11
No files matched your search
@@ -66,7 +66,7 @@ function studentCondition(value: string | null) {
|
|||||||
/**
|
/**
|
||||||
* 方块格:一个班的学生 × 这门课的每一课,打开过没有、学完没有(练一练全对)。
|
* 方块格:一个班的学生 × 这门课的每一课,打开过没有、学完没有(练一练全对)。
|
||||||
*
|
*
|
||||||
* 不选班就按排名页同一套规则猜老师在教哪个班(`defaultClassFor`);也能传年级前缀,
|
* 不选班就猜老师在教哪个班(`defaultClassFor`);也能传年级前缀,
|
||||||
* 但一个年级两三百行方块,页面上主要还是按班看。
|
* 但一个年级两三百行方块,页面上主要还是按班看。
|
||||||
*/
|
*/
|
||||||
adminLearnRoutes.get("/learn-analytics/board", requireTeacher, async (c) => {
|
adminLearnRoutes.get("/learn-analytics/board", requireTeacher, async (c) => {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
classDetail,
|
classDetail,
|
||||||
classPk,
|
classPk,
|
||||||
defaultClassFor,
|
defaultClassFor,
|
||||||
|
lessonClassFor,
|
||||||
loadEntrants,
|
loadEntrants,
|
||||||
PK_MAX_CLASSES,
|
PK_MAX_CLASSES,
|
||||||
pkPartner,
|
pkPartner,
|
||||||
@@ -75,7 +76,7 @@ function periodStart(period: RankPeriod) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 这次请求看的是哪个班 / 哪个年级。学生只能看自己的班和年级;老师用 `className` 选,
|
* 这次请求看的是哪个班 / 哪个年级。学生只能看自己的班和年级;老师用 `className` 选,
|
||||||
* 不给就猜(`defaultClassFor`)。返回 undefined 表示该报「没有班级」。
|
* 不给就取最近布置过作业的班(`lessonClassFor`)。返回 undefined 表示该报「没有班级」。
|
||||||
*/
|
*/
|
||||||
async function resolveGroup(
|
async function resolveGroup(
|
||||||
user: AppEnv["Variables"]["user"],
|
user: AppEnv["Variables"]["user"],
|
||||||
@@ -84,7 +85,7 @@ async function resolveGroup(
|
|||||||
): Promise<string | null | undefined> {
|
): Promise<string | null | undefined> {
|
||||||
if (scope === "all") return null
|
if (scope === "all") return null
|
||||||
let className = user?.className || null
|
let className = user?.className || null
|
||||||
if (isTeacherOrAbove(user)) className = requested || (await defaultClassFor(user!.id))
|
if (isTeacherOrAbove(user)) className = requested || (await lessonClassFor(user!.id))
|
||||||
if (!className) return undefined
|
if (!className) return undefined
|
||||||
return scope === "class" ? className : className.slice(0, 2)
|
return scope === "class" ? className : className.slice(0, 2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,17 +538,26 @@ export async function classPk(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 老师打开排名页(和后台「自学情况」)默认看哪个班:自己最近布置过作业的班 → 最近一周交题人数最多的班。
|
* 老师自己最近布置过作业的班。老师账号没有班级,这是唯一能说明「这是他的班」的数据;
|
||||||
* 不记老师上次选的:统计面板吃过那个亏(上一节课的班悄悄留在框里)。
|
* 排名页点「本班」只认这个,猜不出就交给前端(用上次选的班,没有就停在全部)。
|
||||||
*/
|
*/
|
||||||
export async function defaultClassFor(teacherId: number) {
|
export async function lessonClassFor(teacherId: number) {
|
||||||
const [lesson] = await db
|
const [lesson] = await db
|
||||||
.select({ className: schema.classLesson.className })
|
.select({ className: schema.classLesson.className })
|
||||||
.from(schema.classLesson)
|
.from(schema.classLesson)
|
||||||
.where(eq(schema.classLesson.createdBy, teacherId))
|
.where(eq(schema.classLesson.createdBy, teacherId))
|
||||||
.orderBy(desc(schema.classLesson.day), desc(schema.classLesson.updatedAt))
|
.orderBy(desc(schema.classLesson.day), desc(schema.classLesson.updatedAt))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
if (lesson) return lesson.className
|
return lesson?.className ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台「自学情况」和班级 PK 默认看哪个班:自己最近布置过作业的班 → 最近一周交题人数最多的班。
|
||||||
|
* 不记老师上次选的:统计面板吃过那个亏(上一节课的班悄悄留在框里)。
|
||||||
|
*/
|
||||||
|
export async function defaultClassFor(teacherId: number) {
|
||||||
|
const lesson = await lessonClassFor(teacherId)
|
||||||
|
if (lesson) return lesson
|
||||||
|
|
||||||
const users = countDistinct(schema.submission.userId)
|
const users = countDistinct(schema.submission.userId)
|
||||||
const [busiest] = await db
|
const [busiest] = await db
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import UserName from "shared/components/UserName.vue"
|
|||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { useConfigStore } from "shared/store/config"
|
import { useConfigStore } from "shared/store/config"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { USERNAME_CLASS_RE } from "utils/constants"
|
import { errorCode } from "utils/api"
|
||||||
|
import { STORAGE_KEY, USERNAME_CLASS_RE } from "utils/constants"
|
||||||
|
import storage from "utils/storage"
|
||||||
import ChampionList from "./components/ChampionList.vue"
|
import ChampionList from "./components/ChampionList.vue"
|
||||||
import ClassBattle from "./components/ClassBattle.vue"
|
import ClassBattle from "./components/ClassBattle.vue"
|
||||||
import ClassDetailDrawer from "./components/ClassDetailDrawer.vue"
|
import ClassDetailDrawer from "./components/ClassDetailDrawer.vue"
|
||||||
@@ -60,7 +62,7 @@ function pick<T extends string>(value: unknown, options: { value: T }[], fallbac
|
|||||||
|
|
||||||
const scope = ref<RankScope>("all")
|
const scope = ref<RankScope>("all")
|
||||||
const period = ref<RankPeriod>(pick(route.query.period, PERIOD_OPTIONS, "term"))
|
const period = ref<RankPeriod>(pick(route.query.period, PERIOD_OPTIONS, "term"))
|
||||||
/** 老师选的班;空 = 让后端挑最近上课的班,挑完回填 */
|
/** 老师选的班;空 = 让后端挑最近布置过作业的班,挑完回填 */
|
||||||
const teacherClass = ref(typeof route.query.class === "string" ? route.query.class : "")
|
const teacherClass = ref(typeof route.query.class === "string" ? route.query.class : "")
|
||||||
|
|
||||||
const board = ref<RankBoard | null>(null)
|
const board = ref<RankBoard | null>(null)
|
||||||
@@ -86,13 +88,24 @@ async function load() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
board.value = await fetchBoard(scope.value, period.value)
|
board.value = await fetchBoard(scope.value, period.value)
|
||||||
// 老师在「全部」时点了本班:后端挑了最近上课的班,回填到下拉框(这一下不用再取一遍)
|
// 老师在「全部」时点了本班:后端挑了最近布置过作业的班,回填到下拉框(这一下不用再取一遍)
|
||||||
if (teacher.value && !teacherClass.value && board.value.scope === "class") {
|
if (teacher.value && !teacherClass.value && board.value.scope === "class") {
|
||||||
backfilling = true
|
backfilling = true
|
||||||
teacherClass.value = board.value.className ?? ""
|
teacherClass.value = board.value.className ?? ""
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
board.value = null
|
board.value = null
|
||||||
|
// 没布置过作业、后端猜不出:用上次在下拉框里选的班,没选过就回到全部(watch 会重取)
|
||||||
|
if (teacher.value && !teacherClass.value && errorCode(err) === "class-missing") {
|
||||||
|
loading.value = false
|
||||||
|
const saved = storage.get(STORAGE_KEY.RANK_CLASS)
|
||||||
|
if (typeof saved === "string" && saved) teacherClass.value = saved
|
||||||
|
else {
|
||||||
|
scope.value = "all"
|
||||||
|
message.info("先选一个班")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -171,6 +184,7 @@ const pickedClass = computed({
|
|||||||
get: () => (scope.value === "all" ? "" : teacherClass.value),
|
get: () => (scope.value === "all" ? "" : teacherClass.value),
|
||||||
set: (value: string) => {
|
set: (value: string) => {
|
||||||
teacherClass.value = value
|
teacherClass.value = value
|
||||||
|
if (value) storage.set(STORAGE_KEY.RANK_CLASS, value)
|
||||||
if (!value) scope.value = "all"
|
if (!value) scope.value = "all"
|
||||||
else if (scope.value === "all") scope.value = "class"
|
else if (scope.value === "all") scope.value = "class"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export const STORAGE_KEY = {
|
|||||||
ADMIN_SIDER_COLLAPSED: "adminSiderCollapsed",
|
ADMIN_SIDER_COLLAPSED: "adminSiderCollapsed",
|
||||||
DEMO_MODE: "demoMode",
|
DEMO_MODE: "demoMode",
|
||||||
LOGIN_CLASS: "loginClass",
|
LOGIN_CLASS: "loginClass",
|
||||||
|
RANK_CLASS: "rankClass",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DIFFICULTY = {
|
export const DIFFICULTY = {
|
||||||
|
|||||||
Reference in new issue
Block a user