From 02e2fa7d9bd8223524a2f6246b7fca5804b9ad3c Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Fri, 16 Oct 2026 11:21:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=90=8E=E5=8F=B0):=20=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E4=B8=8A=E4=B9=9F=E8=83=BD=E7=94=A8=E4=BA=86=E2=80=94=E2=80=94?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=94=B6=E8=BF=9B=E5=B7=A6=E8=BE=B9=E6=8A=BD?= =?UTF-8?q?=E5=B1=89=EF=BC=8C=E5=88=97=E8=A1=A8=E4=B8=80=E8=A1=8C=E4=B8=80?= =?UTF-8?q?=E5=BC=A0=E5=8D=A1=EF=BC=8C=E4=B8=8D=E7=94=A8=E5=86=8D=E6=A8=AA?= =?UTF-8?q?=E7=9D=80=E6=BB=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 侧栏手机上不再占一列图标,顶栏左边 ☰ 拉出整张菜单,按返回键先关菜单 - 题目、比赛、用户、公告、题单列表手机上一项一张卡:名字一行、小字一行、按钮一行, 「学生看得到」开关单独一列靠右 - 卡点分析、AI 报告手机上点开一项整屏看,返回键关掉;年度趋势、自学情况、首页改成上下排 - 出题页、教程编辑、题单编辑、标签、设置手机上排成一列;测试数据「删掉」手机上常驻 - 弹框一律收进屏幕宽度 Co-Authored-By: Claude Opus 5.5 --- .../components/AchievementModal.vue | 7 +- apps/web/src/admin/achievement/list.vue | 28 +++ apps/web/src/admin/ai/list.vue | 78 +++++++- apps/web/src/admin/announcement/list.vue | 109 ++++++++--- apps/web/src/admin/components/PageHeader.vue | 2 + .../src/admin/contest/components/Actions.vue | 5 +- apps/web/src/admin/contest/list.vue | 117 ++++++++--- apps/web/src/admin/learn/ExerciseAttempts.vue | 53 ++++- apps/web/src/admin/learn/index.vue | 65 ++++++- apps/web/src/admin/problem/SqlDetail.vue | 17 ++ apps/web/src/admin/problem/Stuck.vue | 183 ++++++++++++++++-- apps/web/src/admin/problem/Trend.vue | 84 +++++++- .../src/admin/problem/components/Actions.vue | 13 +- .../admin/problem/components/CaseTable.vue | 80 ++++++-- .../problem/components/TagProblemsModal.vue | 16 +- apps/web/src/admin/problem/detail.vue | 38 ++++ apps/web/src/admin/problem/list.vue | 91 ++++++++- apps/web/src/admin/problem/tags.vue | 30 +++ apps/web/src/admin/problemset/edit.vue | 16 ++ apps/web/src/admin/problemset/list.vue | 107 ++++++++-- apps/web/src/admin/setting/config.vue | 19 +- apps/web/src/admin/setting/home.vue | 26 ++- .../tutorial/components/ExerciseManager.vue | 1 + apps/web/src/admin/tutorial/detail.vue | 63 +++++- apps/web/src/admin/tutorial/list.vue | 49 ++++- .../web/src/admin/user/components/Actions.vue | 5 +- apps/web/src/admin/user/generate.vue | 31 ++- apps/web/src/admin/user/list.vue | 100 ++++++++-- apps/web/src/index.css | 5 + apps/web/src/shared/layout/admin.vue | 73 +++++-- 30 files changed, 1328 insertions(+), 183 deletions(-) diff --git a/apps/web/src/admin/achievement/components/AchievementModal.vue b/apps/web/src/admin/achievement/components/AchievementModal.vue index 0769239..be68736 100644 --- a/apps/web/src/admin/achievement/components/AchievementModal.vue +++ b/apps/web/src/admin/achievement/components/AchievementModal.vue @@ -10,6 +10,7 @@ import { } from "admin/api" import AchievementIcon from "shared/components/AchievementIcon.vue" import { RARITY_LABEL } from "utils/constants" +import { useBreakpoints } from "shared/composables/breakpoints" const rarityOptions = Object.entries(RARITY_LABEL).map(([value, label]) => ({ label, @@ -44,6 +45,7 @@ function confirmDelete() { } const message = useMessage() +const { isMobile } = useBreakpoints() const metrics = ref([]) const saving = ref(false) @@ -115,7 +117,8 @@ async function save() { :title="editing ? '编辑成就' : '新建成就'" @update:show="emit('update:show', $event)" > - + + @@ -146,7 +149,7 @@ async function save() { - + {{ metricHelp }} diff --git a/apps/web/src/admin/achievement/list.vue b/apps/web/src/admin/achievement/list.vue index c78a78c..60bd89d 100644 --- a/apps/web/src/admin/achievement/list.vue +++ b/apps/web/src/admin/achievement/list.vue @@ -216,4 +216,32 @@ onMounted(load) .off-note { color: v-bind("theme.errorColor"); } + +/* 手机上指标名放在一组上头,下面两档一行;箭头拿掉(折行之后箭头会单独落在行首) */ +@media (max-width: 768px) { + .group { + flex-direction: column; + align-items: stretch; + gap: 8px; + padding: 12px 10px; + } + + .metric { + width: auto; + font-weight: 600; + } + + .steps { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .arrow { + display: none; + } + + .step { + min-width: 0; + } +} diff --git a/apps/web/src/admin/ai/list.vue b/apps/web/src/admin/ai/list.vue index c46b858..d374ef8 100644 --- a/apps/web/src/admin/ai/list.vue +++ b/apps/web/src/admin/ai/list.vue @@ -1,8 +1,11 @@ @@ -150,6 +197,28 @@ watchDebounced(() => query.keyword, listContests, { font-weight: 500; } +.contest-table :deep(.m-card) { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + min-width: 0; +} + +.contest-table :deep(.m-card .title-cell) { + display: flex; + flex-wrap: wrap; +} + +.contest-table :deep(.m-meta) { + font-size: 13px; + line-height: 1.5; +} + +.contest-table :deep(.m-item) { + white-space: nowrap; +} + .contest-table :deep(.exam) { display: inline-flex; align-items: center; diff --git a/apps/web/src/admin/learn/ExerciseAttempts.vue b/apps/web/src/admin/learn/ExerciseAttempts.vue index f57d0a4..9a3307c 100644 --- a/apps/web/src/admin/learn/ExerciseAttempts.vue +++ b/apps/web/src/admin/learn/ExerciseAttempts.vue @@ -3,13 +3,22 @@ import { NText } from "naive-ui" import { getLearnExerciseAttempts } from "admin/api" import { parseTime } from "utils/functions" import type { LearnExerciseAttempt } from "utils/types" +import UserName from "shared/components/UserName.vue" +import { useBreakpoints } from "shared/composables/breakpoints" const props = defineProps<{ exerciseId: number; className: string }>() +const { isMobile } = useBreakpoints() const loading = ref(true) const rows = ref([]) -const columns: DataTableColumn[] = [ +function renderResult(row: LearnExerciseAttempt) { + return row.solved + ? h(NText, { type: "success" }, () => `做对了(第 ${row.attemptsToSolve} 次)`) + : h(NText, { type: "error" }, () => "还没做对") +} + +const desktopColumns: DataTableColumn[] = [ { title: "班级", key: "className", width: 80 }, { title: "学号", key: "username", width: 140 }, { @@ -22,10 +31,7 @@ const columns: DataTableColumn[] = [ title: "结果", key: "solved", width: 130, - render: (row) => - row.solved - ? h(NText, { type: "success" }, () => `做对了(第 ${row.attemptsToSolve} 次)`) - : h(NText, { type: "error" }, () => "还没做对"), + render: renderResult, }, { title: "提交次数", key: "attempts", width: 100, sorter: "default" }, { @@ -45,6 +51,27 @@ const columns: DataTableColumn[] = [ }, ] +/** 手机上七列放不下:一个人一格,名字和结果一行,下面一行小字是交了几次、最后错在哪 */ +const mobileColumns: DataTableColumn[] = [ + { + title: "学生", + key: "username", + render: (row) => + h("div", { class: "m-row" }, [ + h("div", { class: "m-line" }, [h(UserName, { username: row.username }), renderResult(row)]), + h( + NText, + { depth: 3, class: "m-sub" }, + () => + `交了 ${row.attempts} 次 · ${parseTime(row.lastAttemptAt, "M月D日 HH:mm")}` + + (row.lastWrongAnswer ? ` · 最后错在 ${row.lastWrongAnswer}` : ""), + ), + ]), + }, +] + +const columns = computed(() => (isMobile.value ? mobileColumns : desktopColumns)) + onMounted(async () => { try { rows.value = await getLearnExerciseAttempts(props.exerciseId, { @@ -72,3 +99,19 @@ onMounted(async () => { style="padding: 16px 0" /> + + diff --git a/apps/web/src/admin/learn/index.vue b/apps/web/src/admin/learn/index.vue index 7a43c7a..0eb67c0 100644 --- a/apps/web/src/admin/learn/index.vue +++ b/apps/web/src/admin/learn/index.vue @@ -1,5 +1,6 @@ @@ -445,6 +452,26 @@ body{margin:12mm;font-family:"Microsoft YaHei","PingFang SC",sans-serif} gap: 8px; } +@media (max-width: 768px) { + .layout { + flex-direction: column; + align-items: stretch; + } + + .left { + width: auto; + } + + .done-bar { + flex-wrap: wrap; + white-space: normal; + } + + .slips { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + .slip { border: 1px dashed rgba(128, 128, 128, 0.5); border-radius: 4px; diff --git a/apps/web/src/admin/user/list.vue b/apps/web/src/admin/user/list.vue index 53104fb..465f1d8 100644 --- a/apps/web/src/admin/user/list.vue +++ b/apps/web/src/admin/user/list.vue @@ -15,6 +15,7 @@ import { useRouteQuery } from "@vueuse/router" import { NO_CLASS } from "@oj2/contract" import { classLabel, groupClassOptions } from "oj/submission/utils" import { useConfigStore } from "shared/store/config" +import { useBreakpoints } from "shared/composables/breakpoints" /** * 后台用户列表(设计稿「后台用户与批量建号」UsList)。老师进来多半是三件事:学生忘了密码、 @@ -24,6 +25,7 @@ import { useConfigStore } from "shared/store/config" const message = useMessage() const theme = useThemeVars() const configStore = useConfigStore() +const { isMobile } = useBreakpoints() interface UserQuery { keyword: string @@ -91,7 +93,56 @@ function lastLoginText(row: AdminUserForm) { ]) } -const columns: DataTableColumn[] = [ +function renderPassword(row: AdminUserForm) { + return h(PasswordCell, { + user: row, + revealed: revealedPasswords.value.has(row.id), + onToggle: (id: number) => { + if (!revealedPasswords.value.delete(id)) revealedPasswords.value.add(id) + }, + }) +} + +function renderActions(row: AdminUserForm) { + return h(Actions, { + user: row, + onDeleteUser: onDeleteUsers, + onUserBanned, + onOpenEditModal, + onResetPassword, + }) +} + +/** + * 手机上一个人一张卡:用户名一行,班级 · 上次登录 · 建号一行小字(每项不断开), + * 密码单独一行(学生忘了密码是最常见的事,要一眼找到),操作按钮放不下就折行 + */ +function renderMobileCard(row: AdminUserForm) { + return h("div", { class: "m-card" }, [ + h(UserNameCell, { user: row }), + h("div", { class: "m-meta muted" }, [ + h("span", { class: "m-item" }, row.className ? classLabel(row.className) : "没填班级"), + h("span", { class: "m-item" }, [" · ", lastLoginText(row)]), + row.createTime + ? h("span", { class: "m-item" }, ` · 建号 ${parseTime(row.createTime, "YYYY-MM-DD")}`) + : null, + ]), + row.rawPassword + ? h("div", { class: "m-password" }, [ + h("span", { class: "muted" }, "密码"), + renderPassword(row), + ]) + : null, + renderActions(row), + ]) +} + +const mobileColumns: DataTableColumn[] = [ + { type: "selection" }, + { title: "用户", key: "username", render: renderMobileCard }, +] + +const desktopColumns: DataTableColumn[] = [ { type: "selection" }, { title: "用户名", @@ -111,14 +162,7 @@ const columns: DataTableColumn[] = [ title: "密码", key: "raw_password", width: 150, - render: (row) => - h(PasswordCell, { - user: row, - revealed: revealedPasswords.value.has(row.id), - onToggle: (id: number) => { - if (!revealedPasswords.value.delete(id)) revealedPasswords.value.add(id) - }, - }), + render: renderPassword, }, { title: "上次登录", key: "last_login", width: 150, render: lastLoginText }, { @@ -136,17 +180,12 @@ const columns: DataTableColumn[] = [ key: "actions", title: "", minWidth: 300, - render: (row) => - h(Actions, { - user: row, - onDeleteUser: onDeleteUsers, - onUserBanned, - onOpenEditModal, - onResetPassword, - }), + render: renderActions, }, ] +const columns = computed(() => (isMobile.value ? mobileColumns : desktopColumns)) + const options: SelectOption[] = [ { label: "普通", value: USER_TYPE.REGULAR_USER }, { label: "学生管理员", value: USER_TYPE.STUDENT_ADMIN }, @@ -314,7 +353,7 @@ watch(() => [query.page, query.limit, query.type, query.orderBy, query.className