diff --git a/apps/api/src/routes/submission.ts b/apps/api/src/routes/submission.ts index 9f2a294..3adcb40 100644 --- a/apps/api/src/routes/submission.ts +++ b/apps/api/src/routes/submission.ts @@ -371,10 +371,12 @@ submissionRoutes.post("/code/format", requireAuth, async (c) => { } }) +// 参数按「实际用到的字段」声明,而不是整行 $inferSelect:列表接口只 select 需要的列, +// 传不进完整行。完整行在结构上满足这两个窄类型,详情接口照旧调用不受影响。 function canViewSubmission( user: AuthUser | null, - row: typeof schema.submission.$inferSelect, - problem: typeof schema.problem.$inferSelect, + row: { userId: number; shared: boolean }, + problem: { createdById: number; shareSubmission: boolean }, contest: typeof schema.contest.$inferSelect | null, allowShared = true, ) { @@ -385,6 +387,31 @@ function canViewSubmission( return problem.shareSubmission || row.shared } +/** + * 提交列表只取序列化用得到的列。取 `submission.*` / `problem.*` 会把 + * submission.code(学生源码)、info、ip 和 problem 的 description / input_description / + * output_description / hint / samples / answers / flowchart_data / sql_display 一并拉回来, + * 这些字段列表一个都不用,纯属白传。 + */ +const submissionListColumns = { + submission: { + id: schema.submission.id, + createTime: schema.submission.createTime, + userId: schema.submission.userId, + username: schema.submission.username, + result: schema.submission.result, + language: schema.submission.language, + shared: schema.submission.shared, + statisticInfo: schema.submission.statisticInfo, + }, + problem: { + displayId: schema.problem.displayId, + title: schema.problem.title, + shareSubmission: schema.problem.shareSubmission, + createdById: schema.problem.createdById, + }, +} as const + async function submissionDetail(id: string, user: AuthUser) { const [row] = await db.select({ submission: schema.submission, problem: schema.problem, contest: schema.contest }) .from(schema.submission) @@ -441,9 +468,15 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => { if (language) filters.push(eq(schema.submission.language, language)) if (c.req.query("today") === "1") filters.push(sql`${schema.submission.createTime} >= ${todayStart()}`) const where = and(...filters) + // count 不 join problem:problem 只有按题号筛选时才出现在 where 里,无条件 join 会让 + // 计划器把 count 退化成 seq scan(生产快照实测 7.5ms → 78ms)。 + const totalQuery = displayId + ? db.select({ value: count() }).from(schema.submission) + .innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id)).where(where) + : db.select({ value: count() }).from(schema.submission).where(where) const [totalRows, rows] = await Promise.all([ - db.select({ value: count() }).from(schema.submission).innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id)).where(where), - db.select({ submission: schema.submission, problem: schema.problem }).from(schema.submission) + totalQuery, + db.select(submissionListColumns).from(schema.submission) .innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id)).where(where) .orderBy(desc(schema.submission.createTime)).limit(limit).offset(offset), ]) @@ -480,9 +513,15 @@ submissionRoutes.get("/contests/:contestId/submissions", optionalAuth, requireCo if (result !== undefined && result !== "" && Number.isInteger(Number(result))) filters.push(eq(schema.submission.result, Number(result))) if (contestStatus(contest) !== "1") filters.push(sql`${schema.submission.createTime} >= ${contest.startTime}`) const where = and(...filters) + // count 不 join problem:problem 只有按题号筛选时才出现在 where 里,无条件 join 会让 + // 计划器把 count 退化成 seq scan(生产快照实测 7.5ms → 78ms)。 + const totalQuery = displayId + ? db.select({ value: count() }).from(schema.submission) + .innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id)).where(where) + : db.select({ value: count() }).from(schema.submission).where(where) const [totalRows, rows] = await Promise.all([ - db.select({ value: count() }).from(schema.submission).innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id)).where(where), - db.select({ submission: schema.submission, problem: schema.problem }).from(schema.submission) + totalQuery, + db.select(submissionListColumns).from(schema.submission) .innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id)).where(where) .orderBy(desc(schema.submission.createTime)).limit(limit).offset(offset), ]) diff --git a/apps/web/src/oj/submission/list.vue b/apps/web/src/oj/submission/list.vue index 9b38c3f..99dc856 100644 --- a/apps/web/src/oj/submission/list.vue +++ b/apps/web/src/oj/submission/list.vue @@ -23,13 +23,23 @@ import { useUserStore } from "shared/store/user" import { LANGUAGE_SHOW_VALUE } from "utils/constants" import { renderTableTitle } from "utils/renders" import ButtonWithSearch from "./components/ButtonWithSearch.vue" -import StatisticsPanel from "shared/components/StatisticsPanel.vue" -import FlowchartStatisticsPanel from "shared/components/FlowchartStatisticsPanel.vue" import SubmissionLink from "./components/SubmissionLink.vue" -import SubmissionDetail from "./detail.vue" import Grade from "./components/Grade.vue" import FlowchartLink from "./components/FlowchartLink.vue" -import FlowchartScoreDetail from "./components/FlowchartScoreDetail.vue" + +// 下面四个组件只在默认关闭的 n-modal 里用,其中两个统计面板还只有老师看得见。 +// 静态 import 会把它们拖进本路由的关键路径——光 chart.js 就 197KB,进页面前必须先下完。 +// 改成异步后本路由增量下载从 675KB / 59 个文件降到 360KB 出头。 +const StatisticsPanel = defineAsyncComponent( + () => import("shared/components/StatisticsPanel.vue"), +) +const FlowchartStatisticsPanel = defineAsyncComponent( + () => import("shared/components/FlowchartStatisticsPanel.vue"), +) +const SubmissionDetail = defineAsyncComponent(() => import("./detail.vue")) +const FlowchartScoreDetail = defineAsyncComponent( + () => import("./components/FlowchartScoreDetail.vue"), +) interface SubmissionQuery { username: string @@ -51,6 +61,8 @@ const submissions = ref([]) const flowcharts = ref([]) const total = ref(0) const todayCount = ref(0) +// 没有它的话,等接口这段时间表格就是一片空白,连转圈都没有 +const loading = ref(false) // 使用分页 composable const { query, clearQuery } = usePagination({ @@ -99,29 +111,34 @@ const languageOptions: SelectOption[] = [ async function listSubmissions() { if (query.page < 1) query.page = 1 const offset = query.limit * (query.page - 1) - if (query.language === "Flowchart") { - const res = await getFlowchartSubmissions({ - username: query.username, - problemId: query.problem, - myself: query.myself, - offset, - limit: query.limit, - today: query.today, - grade: query.result, - }) - total.value = res.total - flowcharts.value = res.results - } else { - const res = await getSubmissions({ - ...query, - offset, - problemId: query.problem, - contestId: (route.params.contestID as string) ?? "", - language: query.language, - today: query.today, - }) - submissions.value = res.results - total.value = res.total + loading.value = true + try { + if (query.language === "Flowchart") { + const res = await getFlowchartSubmissions({ + username: query.username, + problemId: query.problem, + myself: query.myself, + offset, + limit: query.limit, + today: query.today, + grade: query.result, + }) + total.value = res.total + flowcharts.value = res.results + } else { + const res = await getSubmissions({ + ...query, + offset, + problemId: query.problem, + contestId: (route.params.contestID as string) ?? "", + language: query.language, + today: query.today, + }) + submissions.value = res.results + total.value = res.total + } + } finally { + loading.value = false } } @@ -219,13 +236,11 @@ watch( }, ) -// 登录状态变化后刷新提交列表,更新提交编号列的可点击状态 +// 登录状态变化后刷新提交列表,更新提交编号列的可点击状态。 +// 今日提交数不看登录态,onMounted 那次就够了,这里不用再拉一遍。 watch( () => userStore.isAuthed, - () => { - listSubmissions() - if (route.name === "submissions") getTodayCount() - }, + () => listSubmissions(), ) const columns = computed(() => { @@ -491,12 +506,14 @@ const flowchartColumns = computed(() => { :bordered="false" :columns="flowchartColumns" :data="flowcharts" + :loading="loading" />