refactor(提交列表): 选中翻阅、实时更新、「这节课」、带条件去统计拆成四个 composable

list.vue 从 1834 行减到 1604 行,拆出 oj/submission/composables/ 下的
rowCursor(↑ ↓ 换条、页头页尾自动翻页)、listLive(正在评分的 3 秒刷新、第一页的
「有 N 条新提交」)、lessonFilter(「这节课」一键筛)、statsLink(和统计之间来回带条件)。
注释原样跟着走。

纯搬家。七个状态(列表、↓↓、↑、流程图、我的、手机列表、手机详情)改前改后截图一致;
连按 31 下 ↓ 翻到第 2 页,别人交一条后 18 秒内出「有 1 条新提交」,「这节课」筛到
班级 + 题号 + 今天。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-16 15:48:43 +08:00
1 parent ff3fdc0b1d
commit 871861220e
6 files changed
+423 -275

No files matched your search

@@ -0,0 +1,61 @@
import { computed, ref, type Ref } from "vue"
import { getClassLesson } from "oj/api"
import { classLabel } from "utils/classes"
import type { ClassLesson } from "utils/types"
import type { SubmissionQuery } from "./listQuery"
/** 提交列表的「这节课」:一键筛到 班级 + 这节课的题 + 今天。班级和题的取法同课堂看板 */
export function useLessonFilter(options: {
query: SubmissionQuery
flowMode: Ref<boolean>
info: (text: string) => void
}) {
const { query } = options
const lesson = ref<ClassLesson | null>(null)
const lessonIds = computed(
() => lesson.value?.problems.map((problem) => problem.problemDisplayId).join(",") ?? "",
)
const lessonActive = computed(
() =>
!!lesson.value?.className &&
!!lesson.value.problems.length &&
query.className === lesson.value.className &&
query.problem === lessonIds.value &&
query.today === "1",
)
const lessonLoading = ref(false)
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
try {
const res = await getClassLesson(query.className || undefined)
if (!res.className) {
options.info("猜不出是哪个班在上课,先在班级里选一个")
return
}
query.className = res.className
if (!res.problems.length) {
options.info(
`${classLabel(res.className)}今天还没有布置题,也还没人交。可以先去课堂看板布置`,
)
return
}
lesson.value = res
query.problem = lessonIds.value
query.today = "1"
// 老师布置时选了语言:看板上只数这个语言交的,列表也只列它(流程图那边不动)
if (res.language && !options.flowMode.value) query.language = res.language
} finally {
lessonLoading.value = false
}
}
return { lesson, lessonActive, lessonLoading, toggleLesson }
}
@@ -0,0 +1,90 @@
import { computed, ref, type Ref } from "vue"
import { useDocumentVisibility, useIntervalFn } from "@vueuse/core"
import { SubmissionStatus } from "utils/constants"
import {
FlowchartSubmissionStatus,
type FlowchartSubmissionListItem,
type SubmissionListItem,
} from "utils/types"
import type { Row, SubmissionQuery } from "./listQuery"
function isPending(row: Row) {
if ("result" in row) {
return row.result === SubmissionStatus.pending || row.result === SubmissionStatus.judging
}
return (
row.status === FlowchartSubmissionStatus.PENDING ||
row.status === FlowchartSubmissionStatus.PROCESSING
)
}
/**
* 提交列表的实时更新。两件事,一个定时器:
* - 本页有「正在评分」的,3 秒拉一次,把结果原地换上;
* - 第一页每 15 秒看一眼有没有新交的。**新的不直接插进来** —— 老师正看着第 5 行,
* 上面突然塞进 3 行,选中的那行就跑了。顶上给一条「有 N 条新提交」,点了才换。
* 页面切到后台就停。
*
* `requestSeq` 是列表那边的请求序号:拉的这期间筛选变了、翻页了,这份结果就作废。
*/
export function useListLive(options: {
query: SubmissionQuery
flowMode: Ref<boolean>
rows: Ref<Row[]>
submissions: Ref<SubmissionListItem[]>
flowcharts: Ref<FlowchartSubmissionListItem[]>
loading: Ref<boolean>
loaded: Ref<boolean>
fetchPage: () => Promise<{ results: Row[] }>
requestSeq: () => number
}) {
const { query, flowMode, rows, submissions, flowcharts } = options
const newCount = ref(0)
const newCountCapped = computed(() => newCount.value >= query.limit)
let lastPoll = Date.now()
const visibility = useDocumentVisibility()
const hasPending = computed(() => rows.value.some(isPending))
useIntervalFn(async () => {
if (visibility.value !== "visible" || options.loading.value || !options.loaded.value) return
const interval = hasPending.value ? 3000 : 15000
if (Date.now() - lastPoll < interval) return
if (!hasPending.value && query.page !== 1) return
lastPoll = Date.now()
const seq = options.requestSeq()
const mode = flowMode.value
let res
try {
res = await options.fetchPage()
} catch {
return
}
// 这期间筛选变了、翻页了,这份结果就作废
if (seq !== options.requestSeq() || mode !== flowMode.value) return
const fresh = new Map<string, Row>(res.results.map((row) => [row.id, row]))
if (mode) {
flowcharts.value = flowcharts.value.map(
(row) => (fresh.get(row.id) as FlowchartSubmissionListItem | undefined) ?? row,
)
} else {
submissions.value = submissions.value.map(
(row) => (fresh.get(row.id) as SubmissionListItem | undefined) ?? row,
)
}
if (query.page === 1) {
const current = new Set(rows.value.map((row) => row.id))
const newest = rows.value[0]?.createTime ?? ""
newCount.value = res.results.filter(
(row) => !current.has(row.id) && row.createTime > newest,
).length
}
}, 1000)
/** 刚点了重新判题 / 重新评分:下一秒就拉,不等满 3 秒 */
function pollSoon() {
lastPoll = 0
}
return { newCount, newCountCapped, pollSoon }
}
@@ -0,0 +1,17 @@
import type { FlowchartSubmissionListItem, LANGUAGE, SubmissionListItem } from "utils/types"
/** 提交列表(list.vue)的筛选条件,和它拆出去的几个 composable 共用 */
export interface SubmissionQuery {
page: number
limit: number
username: string
className: string
result: string
myself: "0" | "1"
problem: string
language: LANGUAGE | "" | "Flowchart"
today: "0" | "1"
}
/** 一行:代码提交或流程图提交(看 query.language 是不是 Flowchart) */
export type Row = SubmissionListItem | FlowchartSubmissionListItem
@@ -0,0 +1,131 @@
import { computed, nextTick, ref, type Ref } from "vue"
import { onKeyStroke } from "@vueuse/core"
import type { Row, SubmissionQuery } from "./listQuery"
/**
* 提交列表的「选中与翻阅」:右栏跟着选中的那条走,↑ ↓ 换条,到了页头页尾自动翻页。
*
* 翻页是异步的:先记下「落地后停在哪一头」(pendingJump),新一页回来之后由列表调
* `settleSelection()` 再选。
*/
export function useRowCursor(options: {
rows: Ref<Row[]>
total: Ref<number>
query: SubmissionQuery
loading: Ref<boolean>
isMobile: Ref<boolean>
isDesktop: Ref<boolean>
/** 开着弹框时方向键不归列表 */
keysBlocked: () => boolean
/** 地址里带着的 ?id=(手机上开着详情时刷新、别人发来的链接) */
wantedId: () => string
/** 这一页上没有 ?id= 那一条:详情开不出来,让列表把它从地址上摘掉 */
dropWanted: () => void
/** 跨页落地后这一页一条能看的都没有 */
onNothingToShow: () => void
}) {
const { rows, total, query, loading, isMobile, isDesktop } = options
const selectedId = ref("")
const selectedRow = computed(() => rows.value.find((row) => row.id === selectedId.value) ?? null)
/** 方向键只在能看的行之间走:跳到一条看不了的上面只会看到一把锁 */
const navigable = computed(() => rows.value.filter((row) => row.showLink))
const position = computed(() => {
const index = navigable.value.findIndex((row) => row.id === selectedId.value)
return index === -1 ? null : { index: index + 1, count: navigable.value.length }
})
// 翻页是异步的,先记下「落地后停在哪一头」,新一页回来再选
let pendingJump: "first" | "last" | null = null
/** 加载失败了别把「落地后停在哪一头」留给下一次不相干的加载 */
function forgetJump() {
pendingJump = null
}
function settleSelection() {
const jump = pendingJump
pendingJump = null
const list = navigable.value
if (jump) {
const row = jump === "first" ? list[0] : list[list.length - 1]
if (row) {
select(row.id)
return
}
options.onNothingToShow()
}
// 地址里带着 ?id= 的(手机上开着详情时刷新、别人发来的链接)就停在那一条
const wanted = options.wantedId()
if (wanted && rows.value.some((row) => row.id === wanted)) {
selectedId.value = wanted
return
}
// 这一页上没有那一条:详情开不出来,把 ?id= 摘掉
options.dropWanted()
if (rows.value.some((row) => row.id === selectedId.value)) return
// 手机上没有右栏,不用预先选一条
if (isMobile.value) {
selectedId.value = ""
return
}
selectedId.value = (list[0] ?? rows.value[0])?.id ?? ""
}
function select(id: string) {
selectedId.value = id
nextTick(() => {
document
.querySelector(`[data-row-id="${CSS.escape(id)}"]`)
?.scrollIntoView({ block: "nearest" })
})
}
const maxPage = computed(() => Math.max(1, Math.ceil(total.value / query.limit)))
function move(step: 1 | -1) {
// 这一页一条能看的都没有(学生看全站时常见):不翻页,不然按一下翻一页停不下来
if (!navigable.value.length) return
// 上一次跨页还没回来:按住 ↓ 或者在页尾连按两下,不然会整页跳过
if (loading.value || pendingJump) return
const all = rows.value
const start = all.findIndex((row) => row.id === selectedId.value)
for (let i = start + step; i >= 0 && i < all.length; i += step) {
if (all[i]!.showLink) {
select(all[i]!.id)
return
}
}
const page = query.page + step
if (page < 1 || page > maxPage.value) return
pendingJump = step > 0 ? "first" : "last"
query.page = page
}
// 只用 ↑ ↓(用户定的,← → 不要)
onKeyStroke(["ArrowUp", "ArrowDown"], (e: KeyboardEvent) => {
if (!isDesktop.value || options.keysBlocked()) return
// 别的控件已经处理了这次按键(下拉框自己会开菜单、换选项)
if (e.defaultPrevented) return
const target = e.target as HTMLElement | null
if (target) {
// 「代码 / 流程图」「全部 / 我的」是单选框:点过之后焦点停在上面,浏览器默认会拿方向键
// 切换选项 —— 老师想看下一条,结果模式被切了。单选框照常换条,下面的 preventDefault 挡掉切换
const radio = target.tagName === "INPUT" && (target as HTMLInputElement).type === "radio"
// 焦点在输入框、下拉框、弹框里时不抢方向键
if (
(!radio && (target.tagName === "INPUT" || target.tagName === "TEXTAREA")) ||
target.isContentEditable ||
target.closest(".n-base-selection, .n-base-select-menu, .n-dropdown-menu, .n-modal")
) {
return
}
}
if (!rows.value.length) return
e.preventDefault()
move(e.key === "ArrowUp" ? -1 : 1)
})
return { selectedId, selectedRow, position, maxPage, select, move, settleSelection, forgetJump }
}
@@ -0,0 +1,80 @@
import { reactive, type Ref } from "vue"
import { useRouter } from "vue-router"
import { DEFAULT_PERIOD } from "oj/statistics/period"
import type { SubmissionQuery } from "./listQuery"
/**
* 提交列表和「统计」之间来回带条件:打开统计时带上列表现在的班级 / 学生 / 题号 /
* 代码或流程图,统计里点「在提交列表里看」时把条件改回列表上。
*/
export function useStatsLink(options: {
query: SubmissionQuery
flowMode: Ref<boolean>
statsPanel: Ref<boolean>
/** 列表的「按整名只看一个人」,从统计回来时要放掉 */
exactFor: Ref<string>
}) {
const { query, flowMode, statsPanel } = options
const router = useRouter()
/**
* 「统计」弹框的查询条件。每次打开从这张列表现在的班级 / 学生 / 题号 / 代码或流程图
* 带进去(「今天」带过去就是统计的「今天」);时间段沿用上次在弹框里选的
*/
const statsQuery = reactive({
tab: "code",
className: "",
username: "",
problem: "",
period: DEFAULT_PERIOD as string,
from: "",
to: "",
})
/**
* 老师的「统计」:带着这张列表现在的班级 / 学生 / 题号 / 代码或流程图打开。原来旁边还有一颗
* 「今日统计」,和「数据统计」名字分不清(用户说的),老师那一版并进来了 —— 什么都没筛时
* 打开就是「一行一节课」,选「今天」就是原来今日统计看的东西
*/
function openStatistics() {
Object.assign(statsQuery, {
tab: flowMode.value ? "flow" : "code",
className: query.className,
username: query.username,
problem: query.problem,
...(query.today === "1" ? { period: "today" } : {}),
})
statsPanel.value = true
}
/** 手机上的「统计」:同样的条件,带去统计页 */
function openStatisticsPage() {
router.push({
name: "statistics",
query: {
tab: flowMode.value ? "flow" : "code",
...(query.className ? { className: query.className } : {}),
...(query.username ? { username: query.username } : {}),
...(query.problem ? { problem: query.problem } : {}),
...(query.today === "1" ? { period: "today" } : {}),
},
})
}
/** 统计里的「在提交列表里看」:直接改背后这张列表的筛选,关掉弹框 */
function listFromStatistics(next: Record<string, string>) {
statsPanel.value = false
options.exactFor.value = ""
Object.assign(query, {
username: next.username ?? "",
className: next.className ?? "",
result: "",
myself: "0",
problem: next.problem ?? "",
language: next.language === "Flowchart" ? "Flowchart" : flowMode.value ? "" : query.language,
today: next.today === "1" ? "1" : "0",
})
}
return { statsQuery, openStatistics, openStatisticsPage, listFromStatistics }
}
+44 -275
View File
@@ -5,13 +5,11 @@ import { useRouteQuery } from "@vueuse/router"
import { errorCode } from "utils/api"
import {
adminRejudge,
getClassLesson,
getFlowchartSubmissions,
getSubmissions,
getTodaySubmissionCount,
retryFlowchartSubmission,
} from "oj/api"
import { DEFAULT_PERIOD } from "oj/statistics/period"
import { useContestStore } from "oj/store/contest"
import Pagination from "shared/components/Pagination.vue"
import { useBackClose } from "shared/composables/backClose"
@@ -21,12 +19,7 @@ import { useConfigStore } from "shared/store/config"
import { useUserStore } from "shared/store/user"
import { LANGUAGE_SHOW_VALUE, SubmissionStatus } from "utils/constants"
import { parseTime } from "utils/functions"
import type {
ClassLesson,
FlowchartSubmissionListItem,
LANGUAGE,
SubmissionListItem,
} from "utils/types"
import type { FlowchartSubmissionListItem, LANGUAGE, SubmissionListItem } from "utils/types"
import { FlowchartSubmissionStatus } from "utils/types"
import FlowchartState from "./components/FlowchartState.vue"
import StatusPill from "./components/StatusPill.vue"
@@ -34,6 +27,11 @@ import UserName from "shared/components/UserName.vue"
import { useTone } from "shared/composables/tone"
import { classLabel, classSelectOptions } from "utils/classes"
import { submissionClockText, submissionDayText } from "./utils"
import { useLessonFilter } from "./composables/lessonFilter"
import { useListLive } from "./composables/listLive"
import type { Row, SubmissionQuery } from "./composables/listQuery"
import { useRowCursor } from "./composables/rowCursor"
import { useStatsLink } from "./composables/statsLink"
/**
* 提交列表(设计稿「提交列表重设计 · 定稿」):左边列表、右边常驻选中那条的代码。
@@ -52,16 +50,6 @@ const StatisticsView = defineAsyncComponent(
() => import("oj/statistics/components/StatisticsView.vue"),
)
interface SubmissionQuery {
username: string
className: string
result: string
myself: "0" | "1"
problem: string
language: LANGUAGE | "" | "Flowchart"
today: "0" | "1"
}
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
@@ -87,7 +75,7 @@ let leavingPane = false
const teacher = computed(() => userStore.isTeacherOrAbove)
// 一节课一个班要交一百来条,每页 10 条得翻十几页;30 条配上右栏的方向键正好
const { query, clearQuery } = usePagination<SubmissionQuery>(
const { query, clearQuery } = usePagination<Omit<SubmissionQuery, "page" | "limit">>(
{
username: useRouteQuery("username", "").value,
className: useRouteQuery("className", "").value,
@@ -138,7 +126,6 @@ watch(loading, (value) => {
})
onUnmounted(() => clearTimeout(loadingTimer))
type Row = SubmissionListItem | FlowchartSubmissionListItem
const rows = computed<Row[]>(() => (flowMode.value ? flowcharts.value : submissions.value))
function fetchPage() {
@@ -199,7 +186,7 @@ async function listSubmissions() {
settleSelection()
} catch {
// 失败了别把「落地后停在哪一头」留给下一次不相干的加载
if (seq === requestSeq) pendingJump = null
if (seq === requestSeq) forgetJump()
} finally {
if (seq === requestSeq) loading.value = false
}
@@ -288,106 +275,26 @@ watch(
// ==================== 选中与翻阅 ====================
const selectedId = ref("")
const selectedRow = computed(() => rows.value.find((row) => row.id === selectedId.value) ?? null)
/** 方向键只在能看的行之间走:跳到一条看不了的上面只会看到一把锁 */
const navigable = computed(() => rows.value.filter((row) => row.showLink))
const position = computed(() => {
const index = navigable.value.findIndex((row) => row.id === selectedId.value)
return index === -1 ? null : { index: index + 1, count: navigable.value.length }
})
// 翻页是异步的,先记下「落地后停在哪一头」,新一页回来再选
let pendingJump: "first" | "last" | null = null
function settleSelection() {
const jump = pendingJump
pendingJump = null
const list = navigable.value
if (jump) {
const row = jump === "first" ? list[0] : list[list.length - 1]
if (row) {
select(row.id)
return
}
message.info("这一页没有可以查看的代码")
}
// 地址里带着 ?id= 的(手机上开着详情时刷新、别人发来的链接)就停在那一条
const wanted = typeof route.query.id === "string" ? route.query.id : ""
if (wanted && rows.value.some((row) => row.id === wanted)) {
selectedId.value = wanted
return
}
// 这一页上没有那一条:详情开不出来,把 ?id= 摘掉
const { selectedId, selectedRow, position, maxPage, select, move, settleSelection, forgetJump } =
useRowCursor({
rows,
total,
query,
loading,
isMobile,
isDesktop,
keysBlocked: () => todayPanel.value || statsPanel.value,
wantedId: () => (typeof route.query.id === "string" ? route.query.id : ""),
dropWanted: () => {
if (paneId.value) router.replace({ query: queryWithoutId() })
if (rows.value.some((row) => row.id === selectedId.value)) return
// 手机上没有右栏,不用预先选一条
if (isMobile.value) {
selectedId.value = ""
return
}
selectedId.value = (list[0] ?? rows.value[0])?.id ?? ""
}
function select(id: string) {
selectedId.value = id
nextTick(() => {
document
.querySelector(`[data-row-id="${CSS.escape(id)}"]`)
?.scrollIntoView({ block: "nearest" })
},
onNothingToShow: () => message.info("这一页没有可以查看的代码"),
})
}
const maxPage = computed(() => Math.max(1, Math.ceil(total.value / query.limit)))
function move(step: 1 | -1) {
// 这一页一条能看的都没有(学生看全站时常见):不翻页,不然按一下翻一页停不下来
if (!navigable.value.length) return
// 上一次跨页还没回来:按住 ↓ 或者在页尾连按两下,不然会整页跳过
if (loading.value || pendingJump) return
const all = rows.value
const start = all.findIndex((row) => row.id === selectedId.value)
for (let i = start + step; i >= 0 && i < all.length; i += step) {
if (all[i]!.showLink) {
select(all[i]!.id)
return
}
}
const page = query.page + step
if (page < 1 || page > maxPage.value) return
pendingJump = step > 0 ? "first" : "last"
query.page = page
}
const [todayPanel, toggleTodayPanel] = useToggle(false)
useBackClose(todayPanel, isMobile)
const statsPanel = ref(false)
// 只用 ↑ ↓(用户定的,← → 不要)
onKeyStroke(["ArrowUp", "ArrowDown"], (e: KeyboardEvent) => {
if (!isDesktop.value || todayPanel.value || statsPanel.value) return
// 别的控件已经处理了这次按键(下拉框自己会开菜单、换选项)
if (e.defaultPrevented) return
const target = e.target as HTMLElement | null
if (target) {
// 「代码 / 流程图」「全部 / 我的」是单选框:点过之后焦点停在上面,浏览器默认会拿方向键
// 切换选项 —— 老师想看下一条,结果模式被切了。单选框照常换条,下面的 preventDefault 挡掉切换
const radio = target.tagName === "INPUT" && (target as HTMLInputElement).type === "radio"
// 焦点在输入框、下拉框、弹框里时不抢方向键
if (
(!radio && (target.tagName === "INPUT" || target.tagName === "TEXTAREA")) ||
target.isContentEditable ||
target.closest(".n-base-selection, .n-base-select-menu, .n-dropdown-menu, .n-modal")
) {
return
}
}
if (!rows.value.length) return
e.preventDefault()
move(e.key === "ArrowUp" ? -1 : 1)
})
// 手机上没有右栏:点一行从右边滑出整屏的详情,地址带上 ?id=(见 paneId)
const mobilePane = computed({
get: () => !!paneId.value,
@@ -447,64 +354,17 @@ watch(paneId, (id) => {
// ==================== 实时更新 ====================
/**
* 两件事,一个定时器:
* - 本页有「正在评分」的,3 秒拉一次,把结果原地换上;
* - 第一页每 15 秒看一眼有没有新交的。**新的不直接插进来** —— 老师正看着第 5 行,
* 上面突然塞进 3 行,选中的那行就跑了。顶上给一条「有 N 条新提交」,点了才换。
* 页面切到后台就停。
*/
const newCount = ref(0)
const newCountCapped = computed(() => newCount.value >= query.limit)
let lastPoll = Date.now()
const visibility = useDocumentVisibility()
function isPending(row: Row) {
if ("result" in row) {
return row.result === SubmissionStatus.pending || row.result === SubmissionStatus.judging
}
return (
row.status === FlowchartSubmissionStatus.PENDING ||
row.status === FlowchartSubmissionStatus.PROCESSING
)
}
const hasPending = computed(() => rows.value.some(isPending))
useIntervalFn(async () => {
if (visibility.value !== "visible" || loading.value || !loaded.value) return
const interval = hasPending.value ? 3000 : 15000
if (Date.now() - lastPoll < interval) return
if (!hasPending.value && query.page !== 1) return
lastPoll = Date.now()
const seq = requestSeq
const mode = flowMode.value
let res
try {
res = await fetchPage()
} catch {
return
}
// 这期间筛选变了、翻页了,这份结果就作废
if (seq !== requestSeq || mode !== flowMode.value) return
const fresh = new Map<string, Row>(res.results.map((row) => [row.id, row]))
if (mode) {
flowcharts.value = flowcharts.value.map(
(row) => (fresh.get(row.id) as FlowchartSubmissionListItem | undefined) ?? row,
)
} else {
submissions.value = submissions.value.map(
(row) => (fresh.get(row.id) as SubmissionListItem | undefined) ?? row,
)
}
if (query.page === 1) {
const current = new Set(rows.value.map((row) => row.id))
const newest = rows.value[0]?.createTime ?? ""
newCount.value = res.results.filter(
(row) => !current.has(row.id) && row.createTime > newest,
).length
}
}, 1000)
const { newCount, newCountCapped, pollSoon } = useListLive({
query,
flowMode,
rows,
submissions,
flowcharts,
loading,
loaded,
fetchPage,
requestSeq: () => requestSeq,
})
function showNew() {
newCount.value = 0
@@ -576,50 +436,11 @@ const today = computed({
set: (value: boolean) => (query.today = value ? "1" : "0"),
})
/** 「这节课」:一键筛到 班级 + 这节课的题 + 今天。班级和题的取法同课堂看板 */
const lesson = ref<ClassLesson | null>(null)
const lessonIds = computed(
() => lesson.value?.problems.map((problem) => problem.problemDisplayId).join(",") ?? "",
)
const lessonActive = computed(
() =>
!!lesson.value?.className &&
!!lesson.value.problems.length &&
query.className === lesson.value.className &&
query.problem === lessonIds.value &&
query.today === "1",
)
const lessonLoading = ref(false)
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
try {
const res = await getClassLesson(query.className || undefined)
if (!res.className) {
message.info("猜不出是哪个班在上课,先在班级里选一个")
return
}
query.className = res.className
if (!res.problems.length) {
message.info(`${classLabel(res.className)}今天还没有布置题,也还没人交。可以先去课堂看板布置`)
return
}
lesson.value = res
query.problem = lessonIds.value
query.today = "1"
// 老师布置时选了语言:看板上只数这个语言交的,列表也只列它(流程图那边不动)
if (res.language && !flowMode.value) query.language = res.language
} finally {
lessonLoading.value = false
}
}
const { lesson, lessonActive, lessonLoading, toggleLesson } = useLessonFilter({
query,
flowMode,
info: (text) => message.info(text),
})
const activeFilters = computed(
() =>
@@ -652,65 +473,13 @@ function filterUser(username: string) {
query.username = username
}
/**
* 「统计」弹框的查询条件。每次打开从这张列表现在的班级 / 学生 / 题号 / 代码或流程图
* 带进去(「今天」带过去就是统计的「今天」);时间段沿用上次在弹框里选的
*/
const statsQuery = reactive({
tab: "code",
className: "",
username: "",
problem: "",
period: DEFAULT_PERIOD as string,
from: "",
to: "",
const { statsQuery, openStatistics, openStatisticsPage, listFromStatistics } = useStatsLink({
query,
flowMode,
statsPanel,
exactFor,
})
/**
* 老师的「统计」:带着这张列表现在的班级 / 学生 / 题号 / 代码或流程图打开。原来旁边还有一颗
* 「今日统计」,和「数据统计」名字分不清(用户说的),老师那一版并进来了 —— 什么都没筛时
* 打开就是「一行一节课」,选「今天」就是原来今日统计看的东西
*/
function openStatistics() {
Object.assign(statsQuery, {
tab: flowMode.value ? "flow" : "code",
className: query.className,
username: query.username,
problem: query.problem,
...(query.today === "1" ? { period: "today" } : {}),
})
statsPanel.value = true
}
/** 手机上的「统计」:同样的条件,带去统计页 */
function openStatisticsPage() {
router.push({
name: "statistics",
query: {
tab: flowMode.value ? "flow" : "code",
...(query.className ? { className: query.className } : {}),
...(query.username ? { username: query.username } : {}),
...(query.problem ? { problem: query.problem } : {}),
...(query.today === "1" ? { period: "today" } : {}),
},
})
}
/** 统计里的「在提交列表里看」:直接改背后这张列表的筛选,关掉弹框 */
function listFromStatistics(next: Record<string, string>) {
statsPanel.value = false
exactFor.value = ""
Object.assign(query, {
username: next.username ?? "",
className: next.className ?? "",
result: "",
myself: "0",
problem: next.problem ?? "",
language: next.language === "Flowchart" ? "Flowchart" : flowMode.value ? "" : query.language,
today: next.today === "1" ? "1" : "0",
})
}
/** 右栏「只看这个班」:从看一个人换成看他整个班 */
function filterClass(name: string) {
exactFor.value = ""
@@ -747,7 +516,7 @@ async function rejudge(id: string) {
message.success("已重新判题,结果出来会自己更新")
const row = submissions.value.find((item) => item.id === id)
if (row) row.result = SubmissionStatus.pending
lastPoll = 0
pollSoon()
}
async function retryFlowchart(id: string) {
@@ -766,7 +535,7 @@ async function retryFlowchart(id: string) {
message.success("已重新评分,结果出来会自己更新")
const row = flowcharts.value.find((item) => item.id === id)
if (row) row.status = FlowchartSubmissionStatus.PENDING
lastPoll = 0
pollSoon()
}
/**