From a1d14a06bc8e335b297b8d4ffdc0eafa06792210 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 19 Oct 2026 20:40:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E5=8D=A1=E7=82=B9=E5=88=86=E6=9E=90):?= =?UTF-8?q?=20=E5=8F=B3=E6=A0=8F=E4=B8=80=E9=81=93=E9=A2=98=E7=9A=84?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=8B=86=E6=88=90=20StuckDetail=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stuck.vue 从 1006 行减到 544 行,右栏(做过几人、一次就对、错在哪、各班、交了几次才对、 历年、还没做对的人)连同它的计算和样式挪到 components/StuckDetail.vue;取数、三栏联动、 手机上整屏盖住的那层还在 Stuck.vue。两边都用的百分比和平均次数放 stuckMath.ts, 两边都用的几条小样式(muted / num / track …)各留一份。 纯搬家。全部班级、选一个班、手机打开一道题的详情,各自的顶部和滚到底改前改后截图一致。 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/admin/problem/Stuck.vue | 472 +--------------- .../admin/problem/components/StuckDetail.vue | 524 ++++++++++++++++++ .../src/admin/problem/components/stuckMath.ts | 11 + 3 files changed, 540 insertions(+), 467 deletions(-) create mode 100644 apps/web/src/admin/problem/components/StuckDetail.vue create mode 100644 apps/web/src/admin/problem/components/stuckMath.ts diff --git a/apps/web/src/admin/problem/Stuck.vue b/apps/web/src/admin/problem/Stuck.vue index 92ae0e0..f5b0dfb 100644 --- a/apps/web/src/admin/problem/Stuck.vue +++ b/apps/web/src/admin/problem/Stuck.vue @@ -2,17 +2,16 @@ import { useRouteQuery } from "@vueuse/router" import { Icon } from "@iconify/vue" import { useThemeVars } from "naive-ui" -import { zonedParts } from "utils/functions" -import type { StuckProblem, StuckProblemDetail, StuckStudent } from "utils/types" +import type { StuckProblem, StuckProblemDetail } from "utils/types" import { getStuckProblemDetail, getStuckProblems } from "admin/api" import { useConfigStore } from "shared/store/config" -import UserName from "shared/components/UserName.vue" -import { classLabel, groupClassOptions } from "utils/classes" +import { groupClassOptions } from "utils/classes" import { periodRange, type Period } from "oj/statistics/period" -import { resultName, useResultColor } from "oj/statistics/results" import { useBackClose } from "shared/composables/backClose" import { useBreakpoints } from "shared/composables/breakpoints" import "oj/learn/learn.css" +import StuckDetail from "./components/StuckDetail.vue" +import { avgTries, rate } from "./components/stuckMath" /** * 卡点分析(设计稿「卡点与趋势」页的定稿 StuckFinalAll / StuckFinalClass): @@ -26,7 +25,6 @@ const router = useRouter() const configStore = useConfigStore() const { isMobile } = useBreakpoints() const theme = useThemeVars() -const resultColor = useResultColor() const PERIODS: { label: string; value: Period }[] = [ { label: "这学期", value: "term" }, @@ -53,18 +51,10 @@ const thresholdOptions = computed(() => THRESHOLDS.map((n) => ({ label: `至少 ${n} 人做过`, value: n })), ) -function rate(part: number, whole: number) { - return whole ? Math.round((part / whole) * 100) : 0 -} - function firstRate(row: StuckProblem) { return rate(row.firstTry, row.users) } -function avgTries(row: { solved: number; solvedTries: number }) { - return row.solved ? (row.solvedTries / row.solved).toFixed(1) : "-" -} - function range() { return periodRange(period.value as Period, null).start } @@ -175,117 +165,6 @@ async function pick(row: StuckProblem) { watch([className, period], load) watch(() => [selected.value?.id, problems.value] as const, loadDetail) onMounted(load) - -// ---------------------------------------------------------------- 一道题 - -const students = computed(() => detail.value?.students ?? []) -const summary = computed(() => { - const rows = students.value - const solved = rows.filter((row) => row.solved) - return { - users: rows.length, - firstRate: rate(rows.filter((row) => row.firstTry).length, rows.length), - avgTries: avgTries({ - solved: solved.length, - solvedTries: solved.reduce((sum, row) => sum + row.tries, 0), - }), - stuck: rows.length - solved.length, - } -}) - -const errorTotal = computed(() => - (detail.value?.errors ?? []).reduce((sum, row) => sum + row.count, 0), -) - -const byClass = computed(() => { - const groups = new Map() - for (const row of students.value) { - const key = row.className ?? "" - groups.set(key, [...(groups.get(key) ?? []), row]) - } - return [...groups] - .map(([name, rows]) => { - const solved = rows.filter((row) => row.solved) - return { - name, - users: rows.length, - rate: rate(rows.filter((row) => row.firstTry).length, rows.length), - avgTries: avgTries({ - solved: solved.length, - solvedTries: solved.reduce((sum, row) => sum + row.tries, 0), - }), - stuck: rows.length - solved.length, - } - }) - .sort((a, b) => b.users - a.users) -}) - -const BUCKETS = [ - { label: "1 次", min: 1, max: 1 }, - { label: "2 次", min: 2, max: 2 }, - { label: "3 次", min: 3, max: 3 }, - { label: "4–5 次", min: 4, max: 5 }, - { label: "6 次以上", min: 6, max: Infinity }, -] -const distribution = computed(() => { - const solved = students.value.filter((row) => row.solved) - const rows = BUCKETS.map((bucket) => ({ - ...bucket, - count: solved.filter((row) => row.tries >= bucket.min && row.tries <= bucket.max).length, - })) - const max = Math.max(1, ...rows.map((row) => row.count)) - return rows.map((row) => ({ ...row, height: Math.max(2, (row.count / max) * 64) })) -}) - -const mostTries = computed(() => - students.value - .filter((row) => row.solved && row.tries >= 3) - .sort((a, b) => b.tries - a.tries) - .slice(0, 6), -) - -const stuckStudents = computed(() => - students.value.filter((row) => !row.solved).sort((a, b) => b.tries - a.tries), -) - -const currentYear = (() => { - const now = zonedParts(new Date())! - return now.month >= 9 ? now.year : now.year - 1 -})() -const history = computed(() => - (detail.value?.history ?? []).map((row) => ({ - label: `${String(row.year).slice(2)}–${String(row.year + 1).slice(2)}`, - current: row.year === currentYear, - users: row.users, - rate: rate(row.firstTry, row.users), - })), -) - -function gap(minutes: number | null) { - if (minutes === null) return "" - if (minutes < 1) return "不到 1 分钟" - if (minutes < 60) return `${minutes} 分钟` - if (minutes < 1440) return `${Math.round(minutes / 60)} 小时` - return `隔了 ${Math.round(minutes / 1440)} 天` -} - -function href(to: Parameters[0]) { - return router.resolve(to).href -} - -const links = computed(() => { - const row = detail.value?.problem - if (!row) return null - const scope = className.value ? { className: className.value } : {} - return { - problem: href({ name: "problem", params: { problemID: row.displayId } }), - statistics: href({ - name: "statistics", - query: { problem: row.displayId, period: period.value, ...scope }, - }), - submissions: href({ name: "submissions", query: { problem: row.displayId, ...scope } }), - } -})