From 0ffc405a20f3ce1b1334b10f0d3d36dfe024ba28 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Thu, 15 Oct 2026 14:02:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=89=8D=E5=8F=B0):=20=E5=90=84=E9=A1=B5?= =?UTF-8?q?=E7=9B=B4=E8=BE=BE=E5=90=8E=E5=8F=B0=E5=AF=B9=E5=BA=94=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E2=80=94=E2=80=94=E7=BC=96=E8=BE=91=E8=BF=99=E4=B8=80?= =?UTF-8?q?=E8=AF=BE=E3=80=81=E6=96=B0=E5=BB=BA=E6=AF=94=E8=B5=9B=E3=80=81?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E8=BF=99=E6=9D=A1=E5=85=AC=E5=91=8A=E2=80=A6?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 题目页「编辑题目」从只给超管放宽到有权改这道题的人(含学生管理员改自己出的题); 题目、比赛、题单列表加「新建」;自学首页加「自学情况」「管理教程」,课文加「编辑这一课」; 公告加「编辑这条」「发公告」;成就加「管理成就」;智能分析加「AI 报告」,看某个学生时 带着用户名过去直接筛到他。按钮统一走 shared/components/AdminLink.vue。 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/admin/ai/list.vue | 8 +++++- apps/web/src/oj/achievement/index.vue | 14 ++++++++++ apps/web/src/oj/ai/analysis.vue | 8 ++++++ apps/web/src/oj/announcement/list.vue | 27 ++++++++++++++++++- apps/web/src/oj/contest/list.vue | 7 +++++ apps/web/src/oj/learn/home.vue | 16 +++++++++++ apps/web/src/oj/learn/index.vue | 11 ++++++++ .../src/oj/problem/composables/editorMenu.ts | 3 ++- apps/web/src/oj/problem/list.vue | 4 +++ apps/web/src/oj/problemset/list.vue | 9 +++++++ apps/web/src/shared/components/AdminLink.vue | 19 +++++++++++++ apps/web/src/shared/store/user.ts | 17 ++++++++++++ 12 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/shared/components/AdminLink.vue diff --git a/apps/web/src/admin/ai/list.vue b/apps/web/src/admin/ai/list.vue index e6d62b4..c46b858 100644 --- a/apps/web/src/admin/ai/list.vue +++ b/apps/web/src/admin/ai/list.vue @@ -21,7 +21,13 @@ const theme = useThemeVars() const reports = ref([]) const total = ref(0) -const query = reactive({ limit: 10, page: 1, username: "" }) +// 前台「智能分析」看某个学生时的「AI 报告」按钮带着 ?username= 过来,直接筛到他 +const route = useRoute() +const query = reactive({ + limit: 10, + page: 1, + username: typeof route.query.username === "string" ? route.query.username : "", +}) const selectedId = ref(null) const detail = ref(null) diff --git a/apps/web/src/oj/achievement/index.vue b/apps/web/src/oj/achievement/index.vue index e482705..25a2369 100644 --- a/apps/web/src/oj/achievement/index.vue +++ b/apps/web/src/oj/achievement/index.vue @@ -4,6 +4,7 @@ import { getAchievements } from "oj/achievement/api" import { getUserBadges } from "oj/api" import { useBreakpoints } from "shared/composables/breakpoints" import UserName from "shared/components/UserName.vue" +import AdminLink from "shared/components/AdminLink.vue" import { useUserStore } from "shared/store/user" import { RARITY_COLOR, RARITY_LABEL } from "utils/constants" import type { AchievementList, AchievementRarity, UserBadge } from "utils/types" @@ -83,6 +84,9 @@ watch(name, load) 看我的 › + @@ -168,6 +172,16 @@ h2 { font-variant-numeric: tabular-nums; } +/* 两个都在时「看我的」占着 auto,按钮跟在它后面 */ +.admin-link { + margin-left: auto; + align-self: center; +} + +.mine + .admin-link { + margin-left: 0; +} + .mine { margin-left: auto; font-size: var(--oj-fs-sec); diff --git a/apps/web/src/oj/ai/analysis.vue b/apps/web/src/oj/ai/analysis.vue index a8e366b..4589337 100644 --- a/apps/web/src/oj/ai/analysis.vue +++ b/apps/web/src/oj/ai/analysis.vue @@ -8,6 +8,7 @@ import { import { useRouteQuery } from "@vueuse/router" import { useThemeVars } from "naive-ui" import { useBreakpoints } from "shared/composables/breakpoints" +import AdminLink from "shared/components/AdminLink.vue" import { useUserStore } from "shared/store/user" import UserName from "shared/components/UserName.vue" import { classLabel } from "oj/submission/utils" @@ -137,6 +138,13 @@ function goStudent(username: string | null | undefined) { > 看的提交 › + + AI 报告 + diff --git a/apps/web/src/oj/announcement/list.vue b/apps/web/src/oj/announcement/list.vue index a9571af..75c2ea4 100644 --- a/apps/web/src/oj/announcement/list.vue +++ b/apps/web/src/oj/announcement/list.vue @@ -1,8 +1,10 @@ + + diff --git a/apps/web/src/shared/store/user.ts b/apps/web/src/shared/store/user.ts index f99d6d9..8b34b1b 100644 --- a/apps/web/src/shared/store/user.ts +++ b/apps/web/src/shared/store/user.ts @@ -49,6 +49,22 @@ export const useUserStore = defineStore("user", () => { const canToggleDemoMode = computed(() => realIsSuperAdmin.value) + /** + * 前台「编辑题目」入口给谁看,对齐后端 admin/problem.ts 的 canEdit: + * 公开题看题目的创建者(或题目权限是「全部」),比赛题看比赛的创建者。 + * 题目页拿不到比赛的创建者,比赛题就按题目的创建者估 —— 比赛题基本是出题老师 + * 自己建或克隆的;估错了后台会拦,只是多一个点进去 404 的入口。 + */ + function canEditProblem(problem: { contestId: number | null; createdBy: { id: number } }) { + if (isSuperAdmin.value) return true + const mine = problem.createdBy.id === user.value?.id + if (problem.contestId !== null) return isTeacherOrAbove.value && mine + return ( + hasProblemPermission.value && + (mine || user.value?.problemPermission === PROBLEM_PERMISSION.ALL) + ) + } + function toggleDemoMode() { demoSwitch.value = !demoSwitch.value storage.set(STORAGE_KEY.DEMO_MODE, demoSwitch.value) @@ -124,6 +140,7 @@ export const useUserStore = defineStore("user", () => { hasProblemPermission, demoMode, canToggleDemoMode, + canEditProblem, toggleDemoMode, isAuthed, showSubmissions,