From 2fbcbd07c5a9e194305978654b04ee014512b82e Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 2 Jun 2026 18:13:39 -0600 Subject: [PATCH] feat: update frontend for four-tier role system Add Student Admin and Teacher Admin roles to constants, types, store, permissions, routes, and admin UI. Teacher Admin sees contests and problemsets in sidebar; Student Admin sees only problems. Co-Authored-By: Claude Opus 4.6 --- src/admin/user/components/Name.vue | 2 +- src/admin/user/list.vue | 10 +++++--- src/main.ts | 11 +++++++- src/routes.ts | 22 ++++++++-------- src/shared/components/Header.vue | 2 +- src/shared/layout/admin.vue | 39 +++++++++++++++++++++++++--- src/shared/store/user.ts | 19 +++++++++++--- src/utils/constants.ts | 3 ++- src/utils/functions.ts | 13 +++++++--- src/utils/permissions.ts | 41 +++++++++++++----------------- src/utils/types.ts | 2 +- 11 files changed, 112 insertions(+), 52 deletions(-) diff --git a/src/admin/user/components/Name.vue b/src/admin/user/components/Name.vue index c392363..fb3b704 100644 --- a/src/admin/user/components/Name.vue +++ b/src/admin/user/components/Name.vue @@ -24,7 +24,7 @@ const isNotRegularUser = computed( > {{ getUserRole(props.user.admin_type).label }} - + {{ props.user.problem_permission === PROBLEM_PERMISSION.ALL ? "全部" diff --git a/src/admin/user/list.vue b/src/admin/user/list.vue index 4efd989..ed2ad06 100644 --- a/src/admin/user/list.vue +++ b/src/admin/user/list.vue @@ -38,7 +38,8 @@ const userEditing = ref(null) const adminOptions = [ { label: "全部用户", value: "" }, - { label: "管理员", value: USER_TYPE.ADMIN }, + { label: "学生管理员", value: USER_TYPE.STUDENT_ADMIN }, + { label: "教师管理员", value: USER_TYPE.TEACHER_ADMIN }, { label: "超级管理员", value: USER_TYPE.SUPER_ADMIN }, ] @@ -106,7 +107,8 @@ const columns: DataTableColumn[] = [ const options: SelectOption[] = [ { label: "普通", value: USER_TYPE.REGULAR_USER }, - { label: "管理员", value: USER_TYPE.ADMIN }, + { label: "学生管理员", value: USER_TYPE.STUDENT_ADMIN }, + { label: "教师管理员", value: USER_TYPE.TEACHER_ADMIN }, { label: "超级管理员", value: USER_TYPE.SUPER_ADMIN }, ] @@ -166,7 +168,7 @@ function createNewUser() { username: "", real_name: "", email: "", - admin_type: "Admin", + admin_type: "Student Admin", problem_permission: "None", create_time: new Date(), last_login: new Date(), @@ -312,7 +314,7 @@ watch(() => [query.page, query.limit, query.type, query.orderBy], listUsers) diff --git a/src/main.ts b/src/main.ts index e6b67d0..8ee47c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -40,7 +40,9 @@ router.beforeEach(async (to, from, next) => { if ( to.matched.some( (record) => - record.meta.requiresSuperAdmin || record.meta.requiresProblemPermission, + record.meta.requiresSuperAdmin || + record.meta.requiresTeacherAdmin || + record.meta.requiresProblemPermission, ) ) { if (!storage.get(STORAGE_KEY.AUTHED)) { @@ -63,6 +65,13 @@ router.beforeEach(async (to, from, next) => { next("/") return } + } else if ( + to.matched.some((record) => record.meta.requiresTeacherAdmin) + ) { + if (!userStore.isTeacherOrAbove) { + next("/") + return + } } else if ( to.matched.some((record) => record.meta.requiresProblemPermission) ) { diff --git a/src/routes.ts b/src/routes.ts index f2fc182..a7915ee 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -182,48 +182,48 @@ export const admins: RouteRecordRaw = { path: "contest/list", name: "admin contest list", component: () => import("admin/contest/list.vue"), - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "contest/create", name: "admin contest create", component: () => import("admin/contest/detail.vue"), - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "contest/edit/:contestID", name: "admin contest edit", component: () => import("admin/contest/detail.vue"), props: true, - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "contest/:contestID/problem/list", name: "admin contest problem list", component: () => import("admin/problem/list.vue"), props: true, - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "contest/:contestID/problem/create", name: "admin contest problem create", component: () => import("admin/problem/detail.vue"), props: true, - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "contest/:contestID/problem/edit/:problemID", name: "admin contest problem edit", component: () => import("admin/problem/detail.vue"), props: true, - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "contest/:contestID/helper", name: "admin contest helper", component: () => import("admin/contest/helper.vue"), props: true, - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, // 只有super_admin可以访问的路由 { @@ -293,27 +293,27 @@ export const admins: RouteRecordRaw = { path: "problemset/list", name: "admin problemset list", component: () => import("admin/problemset/list.vue"), - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "problemset/create", name: "admin problemset create", component: () => import("admin/problemset/edit.vue"), - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "problemset/edit/:problemSetId", name: "admin problemset edit", component: () => import("admin/problemset/edit.vue"), props: true, - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, { path: "problemset/:problemSetId", name: "admin problemset detail", component: () => import("admin/problemset/detail.vue"), props: true, - meta: { requiresSuperAdmin: true }, + meta: { requiresTeacherAdmin: true }, }, ], } diff --git a/src/shared/components/Header.vue b/src/shared/components/Header.vue index 8d08f73..b1a0e7f 100644 --- a/src/shared/components/Header.vue +++ b/src/shared/components/Header.vue @@ -166,7 +166,7 @@ const menus = computed(() => [ label: () => h( RouterLink, - { to: userStore.isTheAdmin ? "/admin/problem/list" : "/admin" }, + { to: userStore.isSuperAdmin ? "/admin" : "/admin/problem/list" }, { default: () => "后台" }, ), show: userStore.isAdminRole, diff --git a/src/shared/layout/admin.vue b/src/shared/layout/admin.vue index 635a28a..e7c5669 100644 --- a/src/shared/layout/admin.vue +++ b/src/shared/layout/admin.vue @@ -19,8 +19,8 @@ const options = computed(() => { }, ] - // admin 可以访问的功能 - if (userStore.isTheAdmin) { + // Student Admin: only problems + if (userStore.isStudentAdmin) { baseOptions.push({ label: () => h(RouterLink, { to: "/admin/problem/list" }, { default: () => "题目" }), @@ -28,7 +28,40 @@ const options = computed(() => { }) } - // super_admin 可以访问的功能 + // Teacher Admin: problems + contests + problemsets + if (userStore.isTeacherAdmin) { + baseOptions.push( + { + label: () => + h( + RouterLink, + { to: "/admin/problem/list" }, + { default: () => "题目" }, + ), + key: "admin problem list", + }, + { + label: () => + h( + RouterLink, + { to: "/admin/contest/list" }, + { default: () => "比赛" }, + ), + key: "admin contest list", + }, + { + label: () => + h( + RouterLink, + { to: "/admin/problemset/list" }, + { default: () => "题单" }, + ), + key: "admin problemset list", + }, + ) + } + + // Super Admin: everything if (userStore.isSuperAdmin) { baseOptions.push( { diff --git a/src/shared/store/user.ts b/src/shared/store/user.ts index 320f9e9..d1bbc0d 100644 --- a/src/shared/store/user.ts +++ b/src/shared/store/user.ts @@ -13,10 +13,21 @@ export const useUserStore = defineStore("user", () => { const isAuthed = computed(() => !!user.value?.email) const isAdminRole = computed( () => - user.value?.admin_type === USER_TYPE.ADMIN || + user.value?.admin_type === USER_TYPE.STUDENT_ADMIN || + user.value?.admin_type === USER_TYPE.TEACHER_ADMIN || + user.value?.admin_type === USER_TYPE.SUPER_ADMIN, + ) + const isStudentAdmin = computed( + () => user.value?.admin_type === USER_TYPE.STUDENT_ADMIN, + ) + const isTeacherAdmin = computed( + () => user.value?.admin_type === USER_TYPE.TEACHER_ADMIN, + ) + const isTeacherOrAbove = computed( + () => + user.value?.admin_type === USER_TYPE.TEACHER_ADMIN || user.value?.admin_type === USER_TYPE.SUPER_ADMIN, ) - const isTheAdmin = computed(() => user.value?.admin_type === USER_TYPE.ADMIN) const isSuperAdmin = computed( () => user.value?.admin_type === USER_TYPE.SUPER_ADMIN, ) @@ -47,7 +58,9 @@ export const useUserStore = defineStore("user", () => { isFinished, user, isAdminRole, - isTheAdmin, + isStudentAdmin, + isTeacherAdmin, + isTeacherOrAbove, isSuperAdmin, hasProblemPermission, isAuthed, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 841836d..22e73e7 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -133,7 +133,8 @@ export const CONTEST_TYPE = { export const USER_TYPE = { REGULAR_USER: "Regular User", - ADMIN: "Admin", + STUDENT_ADMIN: "Student Admin", + TEACHER_ADMIN: "Teacher Admin", SUPER_ADMIN: "Super Admin", } diff --git a/src/utils/functions.ts b/src/utils/functions.ts index 372cec1..ec22b3b 100644 --- a/src/utils/functions.ts +++ b/src/utils/functions.ts @@ -133,15 +133,22 @@ export function debounce any>( } export function getUserRole(role: User["admin_type"]): { - type: "default" | "info" | "error" - label: "普通" | "管理员" | "超管" + type: "default" | "info" | "warning" | "error" + label: "普通" | "学生管理员" | "教师管理员" | "超管" } { const roleMap = { [USER_TYPE.REGULAR_USER]: { type: "default" as const, label: "普通" as const, }, - [USER_TYPE.ADMIN]: { type: "info" as const, label: "管理员" as const }, + [USER_TYPE.STUDENT_ADMIN]: { + type: "info" as const, + label: "学生管理员" as const, + }, + [USER_TYPE.TEACHER_ADMIN]: { + type: "warning" as const, + label: "教师管理员" as const, + }, [USER_TYPE.SUPER_ADMIN]: { type: "error" as const, label: "超管" as const, diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 658b9a0..d157c5c 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -1,19 +1,15 @@ import { useUserStore } from "shared/store/user" -/** - * 权限检查工具函数 - */ export function usePermissions() { const userStore = useUserStore() return { - // 基本权限检查 isAuthenticated: computed(() => userStore.isAuthed), isAdminRole: computed(() => userStore.isAdminRole), + isTeacherOrAbove: computed(() => userStore.isTeacherOrAbove), isSuperAdmin: computed(() => userStore.isSuperAdmin), hasProblemPermission: computed(() => userStore.hasProblemPermission), - // 功能权限检查 canManageUsers: computed(() => userStore.isSuperAdmin), canManageAnnouncements: computed(() => userStore.isSuperAdmin), canManageComments: computed(() => userStore.isSuperAdmin), @@ -22,9 +18,10 @@ export function usePermissions() { canSendMessages: computed(() => userStore.isSuperAdmin), canManageProblems: computed(() => userStore.hasProblemPermission), - canManageContests: computed(() => userStore.isSuperAdmin), + canManageContests: computed(() => userStore.isTeacherOrAbove), + canManageProblemsets: computed(() => userStore.isTeacherOrAbove), + canViewClassroomData: computed(() => userStore.isTeacherOrAbove), - // 题目权限细分检查 canManageAllProblems: computed( () => userStore.user?.problem_permission === "All" || userStore.isSuperAdmin, @@ -34,17 +31,15 @@ export function usePermissions() { userStore.user?.problem_permission === "Own" && !userStore.isSuperAdmin, ), - // 获取用户权限级别描述 getUserPermissionLevel: computed(() => { if (userStore.isSuperAdmin) return "超级管理员" - if (userStore.isAdminRole) return "管理员" + if (userStore.isTeacherAdmin) return "教师管理员" + if (userStore.isStudentAdmin) return "学生管理员" return "普通用户" }), - // 获取题目权限描述 getProblemPermissionLevel: computed(() => { if (!userStore.user) return "无权限" - switch (userStore.user.problem_permission) { case "All": return "管理所有题目" @@ -59,13 +54,9 @@ export function usePermissions() { } } -/** - * 路由权限检查 - */ export function checkRoutePermission(routeName: string): boolean { const userStore = useUserStore() - // 需要super admin权限的路由 const superAdminRoutes = [ "admin home", "admin config", @@ -79,35 +70,39 @@ export function checkRoutePermission(routeName: string): boolean { "admin tutorial list", "admin tutorial create", "admin tutorial edit", + ] + + const teacherAdminRoutes = [ "admin contest list", "admin contest create", "admin contest edit", "admin contest problem list", "admin contest problem create", "admin contest problem edit", + "admin contest helper", + "admin problemset list", + "admin problemset create", + "admin problemset edit", + "admin problemset detail", ] - // 需要题目权限的路由 const problemPermissionRoutes = [ "admin problem list", "admin problem create", "admin problem edit", ] - // 需要基本admin权限的路由 - const adminRoutes: string[] = ["admin problem list"] - if (superAdminRoutes.includes(routeName)) { return userStore.isSuperAdmin } + if (teacherAdminRoutes.includes(routeName)) { + return userStore.isTeacherOrAbove + } + if (problemPermissionRoutes.includes(routeName)) { return userStore.hasProblemPermission } - if (adminRoutes.includes(routeName)) { - return userStore.isAdminRole - } - return true } diff --git a/src/utils/types.ts b/src/utils/types.ts index da3c99e..f69b3fd 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -33,7 +33,7 @@ export interface Profile { submission_number: number } -export type UserAdminType = "Regular User" | "Admin" | "Super Admin" +export type UserAdminType = "Regular User" | "Student Admin" | "Teacher Admin" | "Super Admin" export interface User { id: number