From e3eb41288c893de3e60469da8be0e9aab65bc0c8 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 30 Sep 2026 01:20:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8F=90=E4=BA=A4=E5=88=97=E8=A1=A8):=20?= =?UTF-8?q?=E3=80=8C=E6=95=B0=E6=8D=AE=E7=BB=9F=E8=AE=A1=E3=80=8D=E6=94=B9?= =?UTF-8?q?=E5=9B=9E=E5=9C=A8=E5=BD=93=E5=89=8D=E9=A1=B5=E5=BC=B9=E6=A1=86?= =?UTF-8?q?=E9=87=8C=E7=9C=8B=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=BC=80=E6=96=B0?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E9=A1=B5=EF=BC=9B=E6=8C=89=E9=92=AE=E5=92=8C?= =?UTF-8?q?=E3=80=8C=E4=BB=8A=E6=97=A5=E7=BB=9F=E8=AE=A1=E3=80=8D=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=A0=B7=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 弹框带着列表现在的班级、学生、题号打开,「在提交列表里看」直接改背后的列表。 今日统计里点班级名也开同一个弹框。统计主体抽成 StatisticsView,/statistics 页面 留给课堂看板「回头看」、题目页这些别处的入口。 Co-Authored-By: Claude Opus 5.5 --- .../statistics/components/StatisticsView.vue | 487 ++++++++++++++++++ apps/web/src/oj/statistics/index.vue | 427 +-------------- .../submission/components/TodayStatistics.vue | 12 +- apps/web/src/oj/submission/list.vue | 89 +++- 4 files changed, 581 insertions(+), 434 deletions(-) create mode 100644 apps/web/src/oj/statistics/components/StatisticsView.vue diff --git a/apps/web/src/oj/statistics/components/StatisticsView.vue b/apps/web/src/oj/statistics/components/StatisticsView.vue new file mode 100644 index 0000000..74f26dd --- /dev/null +++ b/apps/web/src/oj/statistics/components/StatisticsView.vue @@ -0,0 +1,487 @@ + + + + + diff --git a/apps/web/src/oj/statistics/index.vue b/apps/web/src/oj/statistics/index.vue index 48210f0..26caf81 100644 --- a/apps/web/src/oj/statistics/index.vue +++ b/apps/web/src/oj/statistics/index.vue @@ -1,41 +1,15 @@ @@ -323,125 +44,13 @@ function pickClass(value: string) { .page { margin: -16px; height: calc(100vh - 56px); - display: flex; - flex-direction: column; - background: v-bind("theme.cardColor"); } -.filters { - height: 52px; - flex: none; - box-sizing: border-box; - padding: 0 20px; - display: flex; - align-items: center; - gap: 8px; - white-space: nowrap; - border-bottom: 1px solid v-bind("theme.dividerColor"); -} - -.title { - font-size: 17px; - font-weight: 700; - margin-right: 6px; -} - -.vsep { - width: 1px; - height: 20px; - margin: 0 4px; - background: v-bind("theme.dividerColor"); -} - -.w-class { - width: 132px; -} -.w-user { - width: 110px; -} -.w-problem { - width: 220px; -} -.w-period { - width: 150px; -} -.w-dates { - width: 240px; -} - -@media (max-width: 1365px) { - .w-problem { - width: 190px; - } -} - -.range { - font-size: 12px; - color: v-bind("theme.textColor3"); -} - -.spacer { - flex: 1 1 0; -} - -.summary { - height: 72px; - flex: none; - box-sizing: border-box; - padding: 0 20px; - display: flex; - align-items: center; - gap: 36px; - border-bottom: 1px solid v-bind("theme.dividerColor"); - background: v-bind("theme.actionColor"); -} - -.ring-wrap { - display: flex; - align-items: center; - gap: 8px; -} - -.ring-label { - font-size: 12px; - line-height: 1.5; - color: v-bind("theme.textColor3"); -} - -/* 手机:老师很少在手机上看统计,只保证不坏 —— 筛选和数字行折行,整页自然滚动 */ +/* 手机:老师很少在手机上看统计,只保证不坏 —— 整页自然滚动 */ @media (max-width: 767px) { .page { height: auto; min-height: calc(100vh - 56px); } - - .filters, - .summary { - height: auto; - flex-wrap: wrap; - padding: 10px 14px; - row-gap: 10px; - white-space: normal; - } - - .summary { - gap: 12px 24px; - } - - .vsep, - .spacer { - display: none; - } - - .w-class, - .w-user, - .w-period { - width: calc(50% - 4px); - } - - .w-problem, - .w-dates { - width: 100%; - } } diff --git a/apps/web/src/oj/submission/components/TodayStatistics.vue b/apps/web/src/oj/submission/components/TodayStatistics.vue index 785b8fc..d938b65 100644 --- a/apps/web/src/oj/submission/components/TodayStatistics.vue +++ b/apps/web/src/oj/submission/components/TodayStatistics.vue @@ -16,6 +16,8 @@ import { classLabel } from "../utils" const emit = defineEmits<{ close: [] openProblem: [problem: string] + /** 点班级名:在提交列表的数据统计弹框里看这个班今天 */ + openClass: [className: string] /** 拉到数之后把总数报回去:按钮上的数只在进页面时拉过一次,停久了会对不上 */ loaded: [total: number] }>() @@ -131,14 +133,6 @@ const scatteredText = computed(() => .join("、"), ) -function openClass(className: string) { - const href = router.resolve({ - name: "statistics", - query: { className, period: "today" }, - }).href - window.open(href, "_blank") -} - const judgedResults = computed(() => stats.value?.results ?? []) const languageText = computed(() => { @@ -221,7 +215,7 @@ const meText = computed(() => {
- + {{ classLabel(item.className) }} diff --git a/apps/web/src/oj/submission/list.vue b/apps/web/src/oj/submission/list.vue index 56b5aa6..e8891c8 100644 --- a/apps/web/src/oj/submission/list.vue +++ b/apps/web/src/oj/submission/list.vue @@ -11,6 +11,7 @@ import { getTodaySubmissionCount, retryFlowchartSubmission, } from "oj/api" +import { DEFAULT_PERIOD, type Period } from "oj/statistics/period" import { useContestStore } from "oj/store/contest" import Pagination from "shared/components/Pagination.vue" import { useBreakpoints } from "shared/composables/breakpoints" @@ -44,6 +45,10 @@ import { classLabel, submissionClockText, submissionDayText } from "./utils" const SubmissionPane = defineAsyncComponent(() => import("./components/SubmissionPane.vue")) const FlowchartPane = defineAsyncComponent(() => import("./components/FlowchartPane.vue")) const TodayStatistics = defineAsyncComponent(() => import("./components/TodayStatistics.vue")) +// 统计带着 chart.js,只有老师点开才拉 +const StatisticsView = defineAsyncComponent( + () => import("oj/statistics/components/StatisticsView.vue"), +) interface SubmissionQuery { username: string @@ -313,10 +318,11 @@ function move(step: 1 | -1) { } const [todayPanel, toggleTodayPanel] = useToggle(false) +const statsPanel = ref(false) // 只用 ↑ ↓(用户定的,← → 不要) onKeyStroke(["ArrowUp", "ArrowDown"], (e: KeyboardEvent) => { - if (!isDesktop.value || todayPanel.value) return + if (!isDesktop.value || todayPanel.value || statsPanel.value) return // 别的控件已经处理了这次按键(下拉框自己会开菜单、换选项) if (e.defaultPrevented) return const target = e.target as HTMLElement | null @@ -558,21 +564,44 @@ function filterUser(username: string) { } /** - * 「数据统计」:新标签打开统计页,带上现在的班级 / 学生 / 题号 / 代码或流程图 - * (原来是这一页上的弹框)。「今天」带过去就是统计页的「今天」 + * 「数据统计」弹框的查询条件。每次打开从这张列表现在的班级 / 学生 / 题号 / 代码或流程图 + * 带进去(「今天」带过去就是统计的「今天」);时间段沿用上次在弹框里选的 */ -function openStatistics() { - const href = router.resolve({ - name: "statistics", - query: { - ...(flowMode.value ? { tab: "flow" } : {}), - ...(query.className ? { className: query.className } : {}), - ...(query.username ? { username: query.username } : {}), - ...(query.problem ? { problem: query.problem } : {}), - ...(query.today === "1" ? { period: "today" } : {}), - }, - }).href - window.open(href, "_blank") +const statsQuery = reactive({ + tab: "code", + className: "", + username: "", + problem: "", + period: DEFAULT_PERIOD as string, + from: "", + to: "", +}) + +function openStatistics(preset?: { className: string; period: Period }) { + todayPanel.value = false + Object.assign(statsQuery, { + tab: flowMode.value ? "flow" : "code", + className: preset ? preset.className : query.className, + username: preset ? "" : query.username, + problem: preset ? "" : query.problem, + ...(preset ? { period: preset.period } : query.today === "1" ? { period: "today" } : {}), + }) + statsPanel.value = true +} + +/** 统计里的「在提交列表里看」:直接改背后这张列表的筛选,关掉弹框 */ +function listFromStatistics(next: Record) { + 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", + }) } /** 右栏「只看这个班」:从看一个人换成看他整个班 */ @@ -796,7 +825,13 @@ function dayBreak(index: number) { {{ todayCount }} - + 数据统计 @@ -1074,8 +1109,30 @@ function dayBreak(index: number) { @close="todayPanel = false" @loaded="(total: number) => (todayCount = total)" @open-problem="(id: string) => openProblem({ problemDisplayId: id } as Row)" + @open-class="(name: string) => openStatistics({ className: name, period: 'today' })" /> + + + +
+ +
+