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 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 18:13:39 -06:00
parent 8444d6e21a
commit 2fbcbd07c5
11 changed files with 112 additions and 52 deletions

View File

@@ -24,7 +24,7 @@ const isNotRegularUser = computed(
>
{{ getUserRole(props.user.admin_type).label }}
</n-tag>
<n-tag size="small" v-if="props.user.admin_type === USER_TYPE.ADMIN">
<n-tag size="small" v-if="props.user.admin_type === USER_TYPE.STUDENT_ADMIN || props.user.admin_type === USER_TYPE.TEACHER_ADMIN">
{{
props.user.problem_permission === PROBLEM_PERMISSION.ALL
? "全部"

View File

@@ -38,7 +38,8 @@ const userEditing = ref<User | null>(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<User>[] = [
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)
<n-input v-model:value="password" />
</n-form-item-gi>
<n-form-item-gi
v-if="!create && userEditing.admin_type === USER_TYPE.ADMIN"
v-if="!create && (userEditing.admin_type === USER_TYPE.STUDENT_ADMIN || userEditing.admin_type === USER_TYPE.TEACHER_ADMIN)"
:span="1"
label="出题权限"
>

View File

@@ -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)
) {

View File

@@ -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 },
},
],
}

View File

@@ -166,7 +166,7 @@ const menus = computed<MenuOption[]>(() => [
label: () =>
h(
RouterLink,
{ to: userStore.isTheAdmin ? "/admin/problem/list" : "/admin" },
{ to: userStore.isSuperAdmin ? "/admin" : "/admin/problem/list" },
{ default: () => "后台" },
),
show: userStore.isAdminRole,

View File

@@ -19,8 +19,8 @@ const options = computed<MenuOption[]>(() => {
},
]
// 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<MenuOption[]>(() => {
})
}
// 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(
{

View File

@@ -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,

View File

@@ -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",
}

View File

@@ -133,15 +133,22 @@ export function debounce<T extends (...args: any[]) => 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,

View File

@@ -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
}

View File

@@ -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