Compare commits

..
3 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5.5 e393ffedf7 feat(课堂看板): 加「回头看 →」,带着这个班进数据统计看以前几节课
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:00:38 +08:00
xuyueandClaude Opus 5.5 5f9743efe0 feat(提交列表): 右栏名字旁多一个「只看这个班」,不在班级列表里的老班也能筛出来
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:00:38 +08:00
xuyueandClaude Opus 5.5 e37ad982c0 revert(提交列表): 撤掉班级下拉里补「近半年有提交的班」那一版,下拉回到只用系统设定的班级列表
改成在右栏名字旁加「只看这个班」过滤按钮(下一条)。
This reverts commit 61ec7d7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:00:38 +08:00
6 changed files with 51 additions and 62 deletions

No files matched your search

+3 -30
View File
@@ -208,36 +208,6 @@ submissionRoutes.get("/submissions/today-count", async (c) => {
return success(c, row?.value ?? 0)
})
/**
* 近半年交过代码或流程图的班级,给提交列表的班级下拉补上网站配置里没有的那些。
* 配置的班级列表管的是「注册时能选哪些班」,毕业、换届的班从里面摘掉之后
* 还在交(生产上 247 摘了之后四个月里交了三百多条),下拉里却选不到它。
* 只收 3~4 位数字的班级号,`user.class_name` 里有几个手填的名字不算班。
*/
submissionRoutes.get("/submissions/classes", requireTeacher, async (c) => {
const since = new Date(Date.now() - 180 * 24 * 3600 * 1000).toISOString()
const active = db
.selectDistinct({ userId: schema.submission.userId })
.from(schema.submission)
.where(
and(isNull(schema.submission.contestId), sql`${schema.submission.createTime} >= ${since}`),
)
.union(
db
.selectDistinct({ userId: schema.flowchartSubmission.userId })
.from(schema.flowchartSubmission)
.where(sql`${schema.flowchartSubmission.createTime} >= ${since}`),
)
.as("active")
const rows = await db
.selectDistinct({ className: schema.user.className })
.from(active)
.innerJoin(schema.user, eq(schema.user.id, active.userId))
.where(sql`${schema.user.className} ~ '^[0-9]{3,4}$'`)
.orderBy(schema.user.className)
return success(c, rows.map((row) => row.className!) satisfies string[])
})
submissionRoutes.route("/", submissionStatisticsRoutes)
// 老师也能重判:旧栈只给超管,前端却一直给老师亮着按钮,老师点了只会「权限不足」。
@@ -346,6 +316,7 @@ const submissionListColumns = {
* 已删号的学生 user 表里没有行,退回冻结的那份(否则整列空着)。
*/
username: sql<string>`coalesce(${schema.user.username}, ${schema.submission.username})`,
className: schema.user.className,
result: schema.submission.result,
language: schema.submission.language,
statisticInfo: schema.submission.statisticInfo,
@@ -604,6 +575,7 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => {
createTime: submission.createTime,
userId: submission.userId,
username: submission.username,
className: submission.className || null,
result: submission.result,
language: submission.language,
statisticInfo: asRecord(submission.statisticInfo),
@@ -679,6 +651,7 @@ submissionRoutes.get(
createTime: submission.createTime,
userId: submission.userId,
username: submission.username,
className: submission.className || null,
result: submission.result,
language: submission.language,
statisticInfo: asRecord(submission.statisticInfo),
-5
View File
@@ -167,11 +167,6 @@ export function getTodaySubmissionCount(language?: string) {
return api.get<number>("submissions/today-count", { params: { language } })
}
/** 近半年有提交的班级(老师用),补上网站配置里已经摘掉的班 */
export function getSubmissionClasses() {
return api.get<string[]>("submissions/classes")
}
/** 「今日提交数」标签点开的统计。公开接口,口径同那颗标签:今天 + 非比赛提交 */
export function getTodaySubmissionStatistics() {
return api.get<TodaySubmissionStatistics>("submissions/today-statistics")
+7
View File
@@ -269,6 +269,13 @@ function submissionsHref(
>
</n-text>
<n-button :loading="loading" @click="load">刷新</n-button>
<!-- 看板只管今天;以前几节课的、任意时间段的去数据统计(带上这个班) -->
<n-button
v-if="!projector"
@click="router.push({ name: 'statistics', query: className ? { className } : {} })"
>
回头看 →
</n-button>
<n-button :type="projector ? 'primary' : 'default'" @click="projector = !projector">
{{ projector ? "退出投影" : "投影模式" }}
</n-button>
@@ -19,7 +19,7 @@ import {
import type { Submission, SubmissionListItem } from "utils/types"
import { useCopySubmission } from "../composables/copySubmission"
import { useTone } from "../composables/tone"
import { submissionTimeText } from "../utils"
import { classLabel, submissionTimeText } from "../utils"
import CodeView from "./CodeView.vue"
import StatusPill from "./StatusPill.vue"
import UserName from "./UserName.vue"
@@ -34,6 +34,8 @@ const props = defineProps<{
teacher: boolean
/** 在比赛的提交列表里(不能重判,题目链接走比赛) */
contest: boolean
/** 列表现在筛的班级:已经在筛这个班就不再给「只看这个班」 */
className?: string
/** 右下角的「本页第 i / n 条」 */
position: { index: number; count: number } | null
/** 手机上的整屏详情:头部折行、不提方向键 */
@@ -42,6 +44,7 @@ const props = defineProps<{
const emit = defineEmits<{
filterUser: [username: string]
filterClass: [className: string]
filterProblem: [displayId: string]
openProblem: [row: SubmissionListItem]
rejudge: [id: string]
@@ -243,6 +246,16 @@ function openStandalone() {
>
<Icon icon="ph:funnel-simple-bold" :width="12" />
</button>
<!-- 班级下拉只有网站配置里在读的班,摘掉的老班从这里筛 -->
<button
v-if="teacher && !contest && row.className && row.className !== className"
class="filter-pill wide"
:title="`只看${classLabel(row.className)}的提交`"
:aria-label="`只看${classLabel(row.className)}的提交`"
@click="emit('filterClass', row.className)"
>
{{ classLabel(row.className) }}<Icon icon="ph:funnel-simple-bold" :width="12" />
</button>
</div>
<span class="dot">·</span>
<a
@@ -526,6 +539,12 @@ function openStandalone() {
cursor: pointer;
}
.filter-pill.wide {
width: auto;
padding: 0 8px;
white-space: nowrap;
}
.filter-pill:hover {
background: v-bind("tone('success').background");
}
+15 -26
View File
@@ -7,7 +7,6 @@ import {
adminRejudge,
getClassLesson,
getFlowchartSubmissions,
getSubmissionClasses,
getSubmissions,
getTodaySubmissionCount,
retryFlowchartSubmission,
@@ -445,33 +444,11 @@ const languageOptions = [
{ label: "SQL", value: "SQL" },
]
// 配置里的班级列表是「注册时能选的班」,摘掉的老班还在交,得从提交里补出来
const activeClasses = ref<string[]>([])
watch(
teacher,
async (isTeacher) => {
if (!isTeacher || activeClasses.value.length) return
try {
activeClasses.value = await getSubmissionClasses()
} catch {
// 补不上就只有配置里那些,不打扰
}
},
{ immediate: true },
)
const classOptions = computed(() => {
const list = configStore.config?.classList ?? []
const others = activeClasses.value.filter((item) => !list.includes(item)).reverse()
// 带进来的班级哪边都没有(手打的)也要显示得出来,不然框里是空的却在筛
if (query.className && !list.includes(query.className) && !others.includes(query.className))
others.unshift(query.className)
const option = (item: string) => ({ label: classLabel(item), value: item })
if (!others.length) return list.map(option)
return [
{ type: "group", label: "班级列表", key: "listed", children: list.map(option) },
{ type: "group", label: "不在班级列表里", key: "others", children: others.map(option) },
]
// 带进来的班级不在配置里(老班、手打的)也要显示得出来,不然框里是空的却在筛
const all = query.className && !list.includes(query.className) ? [query.className, ...list] : list
return all.map((item) => ({ label: classLabel(item), value: item }))
})
const contestProblemOptions = computed(() => [
@@ -598,6 +575,14 @@ function openStatistics() {
window.open(href, "_blank")
}
/** 右栏「只看这个班」:从看一个人换成看他整个班 */
function filterClass(name: string) {
exactFor.value = ""
query.username = ""
query.myself = "0"
query.className = name
}
function filterProblem(displayId: string) {
query.problem = displayId
}
@@ -1011,9 +996,11 @@ function dayBreak(index: number) {
v-else
:row="selectedRow as SubmissionListItem"
:teacher="teacher"
:class-name="query.className"
:contest="inContest"
:position="position"
@filter-user="filterUser"
@filter-class="filterClass"
@filter-problem="filterProblem"
@open-problem="openProblem"
@rejudge="rejudge"
@@ -1047,10 +1034,12 @@ function dayBreak(index: number) {
v-else
:row="selectedRow as SubmissionListItem"
:teacher="teacher"
:class-name="query.className"
:contest="inContest"
:position="null"
narrow
@filter-user="(name: string) => ((mobilePane = false), filterUser(name))"
@filter-class="(name: string) => ((mobilePane = false), filterClass(name))"
@filter-problem="(id: string) => ((mobilePane = false), filterProblem(id))"
@open-problem="openProblem"
@rejudge="rejudge"
+6
View File
@@ -271,6 +271,12 @@ export const submissionListItemSchema = z.object({
createTime: z.string(),
userId: z.number().int(),
username: z.string(),
/**
* 提交人现在的班级(user.class_name),没有班级、已删号的为 null。右栏的「只看这个班」
* 用它 —— 网站配置里的班级列表只有在读的班,摘掉的老班在下拉里选不到,从一条提交
* 点过去就能筛
*/
className: z.string().nullable(),
result: judgeStatusSchema,
language: problemLanguageSchema,
statisticInfo: statisticInfoSchema,