From efbca0e80284314bb1429b406e2b9b482a1e8509 Mon Sep 17 00:00:00 2001
From: yuetsh <517252939@qq.com>
Date: Thu, 25 Sep 2025 18:41:32 +0800
Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=94=A8=E6=88=B7=E6=9D=83?=
=?UTF-8?q?=E9=99=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/PERMISSIONS.md | 123 +++++++++++++++++
src/admin/api.ts | 4 +-
src/admin/setting/config.vue | 9 ++
src/admin/setting/home.vue | 8 ++
src/admin/user/components/Name.vue | 7 +-
src/admin/user/list.vue | 75 +++++++++--
src/main.ts | 61 ++++++++-
src/oj/submission/list.vue | 26 ++--
src/routes.ts | 25 ++++
src/shared/components/Header.vue | 7 +-
src/shared/layout/admin.vue | 135 ++++++++++++-------
src/shared/store/user.ts | 4 +
src/utils/functions.ts | 203 ++++++++++++++---------------
src/utils/permissions.ts | 113 ++++++++++++++++
src/utils/types.ts | 6 +-
15 files changed, 619 insertions(+), 187 deletions(-)
create mode 100644 docs/PERMISSIONS.md
create mode 100644 src/utils/permissions.ts
diff --git a/docs/PERMISSIONS.md b/docs/PERMISSIONS.md
new file mode 100644
index 0000000..278e51e
--- /dev/null
+++ b/docs/PERMISSIONS.md
@@ -0,0 +1,123 @@
+# 前端权限控制说明
+
+本文档说明了前端如何根据后端权限设计实现权限控制。
+
+## 权限等级
+
+### 1. 普通用户 (Regular User)
+- admin_type: "Regular User"
+- problem_permission: "None"
+- 只能访问前台功能,无法进入管理后台
+
+### 2. 管理员 (Admin)
+- admin_type: "Admin"
+- problem_permission: "None" | "Own" | "All"
+- 可以访问部分管理功能
+
+### 3. 超级管理员 (Super Admin)
+- admin_type: "Super Admin"
+- problem_permission: "All" (自动设置)
+- 可以访问所有管理功能
+
+## 权限控制实现
+
+### 1. 用户状态管理 (`src/shared/store/user.ts`)
+```typescript
+const isAdminRole = computed(() =>
+ user.value?.admin_type === USER_TYPE.ADMIN ||
+ user.value?.admin_type === USER_TYPE.SUPER_ADMIN
+)
+const isSuperAdmin = computed(() =>
+ user.value?.admin_type === USER_TYPE.SUPER_ADMIN
+)
+const hasProblemPermission = computed(() =>
+ user.value?.problem_permission !== PROBLEM_PERMISSION.NONE
+)
+```
+
+### 2. 权限工具函数 (`src/utils/permissions.ts`)
+提供了一系列权限检查函数:
+- `canManageUsers`: 只有super_admin可以管理用户
+- `canManageProblems`: 需要题目权限
+- `canManageSystemConfig`: 只有super_admin可以管理系统配置
+- 等等...
+
+### 3. 路由权限控制 (`src/main.ts`)
+在路由守卫中检查权限:
+- `requiresAdmin`: 需要admin或super_admin权限
+- `requiresSuperAdmin`: 只有super_admin可以访问
+- `requiresProblemPermission`: 需要题目管理权限
+
+### 4. 菜单权限控制 (`src/shared/layout/admin.vue`)
+根据用户权限动态显示菜单项:
+- admin和super_admin都可以看到:题目管理
+- 只有super_admin可以看到:比赛管理、用户管理、系统设置、公告管理、评论管理、教程管理
+
+## 各功能模块权限说明
+
+### 用户管理 (需要 super_admin)
+- 创建、编辑、删除用户
+- 重置用户密码
+- 封禁/解封用户
+- 批量导入用户
+
+### 题目管理 (需要 problem_permission)
+- `problem_permission = "None"`: 无法管理题目
+- `problem_permission = "Own"`: 只能管理自己创建的题目
+- `problem_permission = "All"`: 可以管理所有题目
+
+### 比赛管理 (super_admin)
+- 创建、编辑、删除比赛
+- 管理比赛题目
+- 查看比赛数据
+
+### 系统配置 (需要 super_admin)
+- SMTP邮件配置
+- 判题服务器管理
+- 网站基本设置
+- 测试用例清理
+
+### 公告管理 (需要 super_admin)
+- 创建、编辑、删除公告
+
+### 评论管理 (需要 super_admin)
+- 查看、删除用户评论
+
+### 教程管理 (需要 super_admin)
+- 创建、编辑、删除教程
+
+## 权限检查流程
+
+1. **路由级权限检查**: 在 `main.ts` 的路由守卫中进行
+2. **组件级权限检查**: 在组件内部使用 `usePermissions()` 进行检查
+3. **UI权限控制**: 根据权限动态显示/隐藏UI元素
+
+## 使用示例
+
+```vue
+
+
+
+
+
+ 超级管理员专用功能
+
+
+```
+
+## 注意事项
+
+1. **前端权限控制只是UI层面的限制**,真正的权限控制在后端
+2. **所有API调用都会在后端进行权限验证**
+3. **权限信息来自后端用户接口**,前端不应该缓存或修改权限信息
+4. **路由权限检查是异步的**,需要等待用户信息加载完成
diff --git a/src/admin/api.ts b/src/admin/api.ts
index 72d6338..f9bfb73 100644
--- a/src/admin/api.ts
+++ b/src/admin/api.ts
@@ -84,11 +84,11 @@ export function getContestProblem(id: number) {
export function getUserList(
offset = 0,
limit = 10,
- admin = "0",
+ type = "",
keyword: string,
) {
return http.get("admin/user", {
- params: { paging: true, offset, limit, keyword, admin },
+ params: { paging: true, offset, limit, keyword, type },
})
}
diff --git a/src/admin/setting/config.vue b/src/admin/setting/config.vue
index 35ae7e9..68c4a00 100644
--- a/src/admin/setting/config.vue
+++ b/src/admin/setting/config.vue
@@ -2,6 +2,7 @@
import { NButton, NTag } from "naive-ui"
import { parseTime } from "~/utils/functions"
import { Server } from "~/utils/types"
+import { usePermissions } from "~/utils/permissions"
import {
deleteJudgeServer,
editWebsite,
@@ -17,6 +18,14 @@ interface Testcase {
}
const message = useMessage()
+const router = useRouter()
+const { canManageSystemConfig } = usePermissions()
+
+// 权限检查:只有super_admin可以管理系统配置
+if (!canManageSystemConfig.value) {
+ message.error("您没有权限访问此页面")
+ router.push("/admin")
+}
const testcaseColumns: DataTableColumn[] = [
{ title: "测试用例 ID", key: "id" },
diff --git a/src/admin/setting/home.vue b/src/admin/setting/home.vue
index a87dcdd..2f8bf12 100644
--- a/src/admin/setting/home.vue
+++ b/src/admin/setting/home.vue
@@ -3,6 +3,7 @@ import { NButton } from "naive-ui"
import { getRank } from "oj/api"
import Pagination from "~/shared/components/Pagination.vue"
import { useUserStore } from "~/shared/store/user"
+import { usePermissions } from "~/utils/permissions"
import { getACRate } from "~/utils/functions"
import { Rank } from "~/utils/types"
import { getBaseInfo, randomUser10 } from "../api"
@@ -13,6 +14,13 @@ const contestCount = ref(0)
const userStore = useUserStore()
const router = useRouter()
const message = useMessage()
+const { isSuperAdmin } = usePermissions()
+
+// 权限检查:只有super_admin可以访问管理员首页
+if (!isSuperAdmin.value) {
+ message.error("您没有权限访问此页面")
+ router.push("/admin/problem/list")
+}
const showModal = ref(false)
const luckyGuy = ref("")
diff --git a/src/admin/user/components/Name.vue b/src/admin/user/components/Name.vue
index 0d77f98..24722de 100644
--- a/src/admin/user/components/Name.vue
+++ b/src/admin/user/components/Name.vue
@@ -1,4 +1,5 @@
@@ -14,7 +17,7 @@ const isAdmin = computed(() => props.user.admin_type !== "Regular User")
封号中
diff --git a/src/admin/user/list.vue b/src/admin/user/list.vue
index 848f020..5e42bf7 100644
--- a/src/admin/user/list.vue
+++ b/src/admin/user/list.vue
@@ -1,8 +1,9 @@
@@ -200,8 +249,12 @@ watch(query, listUsers, { deep: true })
确定删除选中的用户吗?删除后无法恢复!
- 超管出列
-
+
diff --git a/src/main.ts b/src/main.ts
index 350b04a..e4f2b2d 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -31,17 +31,68 @@ const router = createRouter({
routes: [ojs, admins],
})
-router.beforeEach((to, from, next) => {
+router.beforeEach(async (to, from, next) => {
+ // 检查是否需要认证
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!storage.get(STORAGE_KEY.AUTHED)) {
toggleLogin(true)
next("/")
- } else {
- next()
+ return
}
- } else {
- next()
}
+
+ // 检查管理员权限
+ if (to.matched.some((record) =>
+ record.meta.requiresAdmin ||
+ record.meta.requiresSuperAdmin ||
+ record.meta.requiresProblemPermission
+ )) {
+ if (!storage.get(STORAGE_KEY.AUTHED)) {
+ toggleLogin(true)
+ next("/")
+ return
+ }
+
+ // 动态导入用户store来检查权限
+ const { useUserStore } = await import("./shared/store/user")
+ const userStore = useUserStore()
+
+ // 确保用户信息已加载
+ if (!userStore.user) {
+ try {
+ await userStore.getMyProfile()
+ } catch (error) {
+ next("/")
+ return
+ }
+ }
+
+ // 检查super admin权限
+ if (to.matched.some((record) => record.meta.requiresSuperAdmin)) {
+ if (!userStore.isSuperAdmin) {
+ next("/admin")
+ return
+ }
+ }
+
+ // 检查题目权限
+ else if (to.matched.some((record) => record.meta.requiresProblemPermission)) {
+ if (!userStore.hasProblemPermission) {
+ next("/admin")
+ return
+ }
+ }
+
+ // 检查基本admin权限
+ else if (to.matched.some((record) => record.meta.requiresAdmin)) {
+ if (!userStore.isAdminRole) {
+ next("/")
+ return
+ }
+ }
+ }
+
+ next()
})
ChartJS.register(
diff --git a/src/oj/submission/list.vue b/src/oj/submission/list.vue
index af36963..0bea60e 100644
--- a/src/oj/submission/list.vue
+++ b/src/oj/submission/list.vue
@@ -70,16 +70,22 @@ async function listSubmissions() {
if (query.page < 1) query.page = 1
const offset = query.limit * (query.page - 1)
- const res = await getSubmissions({
- ...query,
- myself: query.myself ? "1" : "0",
- offset,
- problem_id: route.query.problem ?? "",
- contest_id: route.params.contestID ?? "",
- language: query.language,
- })
- submissions.value = res.data.results
- total.value = res.data.total
+ try {
+ const res = await getSubmissions({
+ ...query,
+ myself: query.myself ? "1" : "0",
+ offset,
+ problem_id: route.query.problem ?? "",
+ contest_id: route.params.contestID ?? "",
+ language: query.language,
+ })
+ submissions.value = res.data.results
+ total.value = res.data.total
+ } catch (error: any) {
+ if (error.data === "Problem doesn't exist") {
+ message.error("题目不存在")
+ }
+ }
}
async function getTodayCount() {
diff --git a/src/routes.ts b/src/routes.ts
index 9f996ad..53be434 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -107,113 +107,138 @@ export const admins: RouteRecordRaw = {
path: "",
name: "admin home",
component: () => import("~/admin/setting/home.vue"),
+ meta: { requiresAdmin: true },
},
+ // 只有super_admin可以访问的路由
{
path: "config",
name: "admin config",
component: () => import("admin/setting/config.vue"),
+ meta: { requiresSuperAdmin: true },
},
{
path: "user/list",
name: "admin user list",
component: () => import("admin/user/list.vue"),
+ meta: { requiresSuperAdmin: true },
},
{
path: "user/generate",
name: "admin user generate",
component: () => import("~/admin/user/generate.vue"),
+ meta: { requiresSuperAdmin: true },
},
+ // admin和super_admin都可以访问的路由 (需要题目权限)
{
path: "problem/list",
name: "admin problem list",
component: () => import("admin/problem/list.vue"),
+ meta: { requiresProblemPermission: true },
},
{
path: "problem/create",
name: "admin problem create",
component: () => import("admin/problem/detail.vue"),
+ meta: { requiresProblemPermission: true },
},
{
path: "problem/edit/:problemID",
name: "admin problem edit",
component: () => import("admin/problem/detail.vue"),
props: true,
+ meta: { requiresProblemPermission: true },
},
+ // admin和super_admin都可以访问的路由
{
path: "contest/list",
name: "admin contest list",
component: () => import("admin/contest/list.vue"),
+ meta: { requiresAdmin: true },
},
{
path: "contest/create",
name: "admin contest create",
component: () => import("admin/contest/detail.vue"),
+ meta: { requiresAdmin: true },
},
{
path: "contest/edit/:contestID",
name: "admin contest edit",
component: () => import("admin/contest/detail.vue"),
props: true,
+ meta: { requiresAdmin: true },
},
{
path: "contest/:contestID/problem/list",
name: "admin contest problem list",
component: () => import("admin/problem/list.vue"),
props: true,
+ meta: { requiresAdmin: true },
},
{
path: "contest/:contestID/problem/create",
name: "admin contest problem create",
component: () => import("admin/problem/detail.vue"),
props: true,
+ meta: { requiresAdmin: true },
},
{
path: "contest/:contestID/problem/edit/:problemID",
name: "admin contest problem edit",
component: () => import("admin/problem/detail.vue"),
props: true,
+ meta: { requiresAdmin: true },
},
+ // 只有super_admin可以访问的路由
{
path: "announcement/list",
name: "admin announcement list",
component: () => import("admin/announcement/list.vue"),
+ meta: { requiresSuperAdmin: true },
},
{
path: "announcement/create",
name: "admin announcement create",
component: () => import("admin/announcement/detail.vue"),
+ meta: { requiresSuperAdmin: true },
},
{
path: "announcement/edit/:announcementID",
name: "admin announcement edit",
component: () => import("admin/announcement/detail.vue"),
props: true,
+ meta: { requiresSuperAdmin: true },
},
{
path: "comment/list",
name: "admin comment list",
component: () => import("admin/communication/comments.vue"),
+ meta: { requiresSuperAdmin: true },
},
{
path: "message/list",
name: "admin message list",
component: () => import("admin/communication/messages.vue"),
+ meta: { requiresSuperAdmin: true },
},
{
path: "tutorial/list",
name: "admin tutorial list",
component: () => import("admin/tutorial/list.vue"),
+ meta: { requiresSuperAdmin: true },
},
{
path: "tutorial/create",
name: "admin tutorial create",
component: () => import("admin/tutorial/detail.vue"),
+ meta: { requiresSuperAdmin: true },
},
{
path: "tutorial/edit/:tutorialID",
name: "admin tutorial edit",
component: () => import("admin/tutorial/detail.vue"),
props: true,
+ meta: { requiresSuperAdmin: true },
},
],
}
diff --git a/src/shared/components/Header.vue b/src/shared/components/Header.vue
index 7fe4390..ef4f44e 100644
--- a/src/shared/components/Header.vue
+++ b/src/shared/components/Header.vue
@@ -87,7 +87,12 @@ const menus = computed(() => [
icon: renderIcon("streamline-emojis:palm-tree"),
},
{
- label: () => h(RouterLink, { to: "/admin" }, { default: () => "后台" }),
+ label: () =>
+ h(
+ RouterLink,
+ { to: userStore.isTheAdmin ? "/admin/problem/list" : "/admin" },
+ { default: () => (userStore.isTheAdmin ? "我出的题" : "后台") },
+ ),
show: userStore.isAdminRole,
key: "admin",
icon: renderIcon("streamline-emojis:ghost"),
diff --git a/src/shared/layout/admin.vue b/src/shared/layout/admin.vue
index 99db6c3..4792b4c 100644
--- a/src/shared/layout/admin.vue
+++ b/src/shared/layout/admin.vue
@@ -7,55 +7,92 @@ import { useUserStore } from "../store/user"
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
-const options: MenuOption[] = [
- {
- label: () => h(RouterLink, { to: "/" }, { default: () => "前台" }),
- key: "return to OJ",
- },
- {
- label: () => h(RouterLink, { to: "/admin" }, { default: () => "管理" }),
- key: "admin home",
- },
- {
- label: () =>
- h(RouterLink, { to: "/admin/config" }, { default: () => "设置" }),
- key: "admin config",
- },
- {
- label: () =>
- h(RouterLink, { to: "/admin/problem/list" }, { default: () => "题目" }),
- key: "admin problem list",
- },
- {
- label: () =>
- h(RouterLink, { to: "/admin/comment/list" }, { default: () => "评论" }),
- key: "admin comment list",
- },
- {
- label: () =>
- h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户" }),
- key: "admin user list",
- },
- {
- label: () =>
- h(RouterLink, { to: "/admin/contest/list" }, { default: () => "比赛" }),
- key: "admin contest list",
- },
- {
- label: () =>
- h(
- RouterLink,
- { to: "/admin/announcement/list" },
- { default: () => "公告" },
- ),
- key: "admin announcement list",
- },
- {
- label: () =>
- h(RouterLink, { to: "/admin/tutorial/list" }, { default: () => "教程" }),
- key: "admin tutorial list",
- },
-]
+
+// 根据用户权限动态生成菜单选项
+const options = computed(() => {
+ const baseOptions: MenuOption[] = [
+ {
+ label: () => h(RouterLink, { to: "/" }, { default: () => "前台" }),
+ key: "return to OJ",
+ },
+ ]
+
+ // admin 可以访问的功能
+ if (userStore.isTheAdmin) {
+ baseOptions.push({
+ label: () =>
+ h(RouterLink, { to: "/admin/problem/list" }, { default: () => "题目" }),
+ key: "admin problem list",
+ })
+ }
+
+ // super_admin 可以访问的功能
+ if (userStore.isSuperAdmin) {
+ baseOptions.push(
+ {
+ label: () => h(RouterLink, { to: "/admin" }, { default: () => "管理" }),
+ key: "admin home",
+ },
+ {
+ 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/config" }, { default: () => "设置" }),
+ key: "admin config",
+ },
+ {
+ label: () =>
+ h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户" }),
+ key: "admin user list",
+ },
+ {
+ label: () =>
+ h(
+ RouterLink,
+ { to: "/admin/comment/list" },
+ { default: () => "评论" },
+ ),
+ key: "admin comment list",
+ },
+ {
+ label: () =>
+ h(
+ RouterLink,
+ { to: "/admin/announcement/list" },
+ { default: () => "公告" },
+ ),
+ key: "admin announcement list",
+ },
+ {
+ label: () =>
+ h(
+ RouterLink,
+ { to: "/admin/tutorial/list" },
+ { default: () => "教程" },
+ ),
+ key: "admin tutorial list",
+ },
+ )
+ }
+
+ return baseOptions
+})
const active = computed(() => (route.name as string) || "home")
diff --git a/src/shared/store/user.ts b/src/shared/store/user.ts
index 679d8b1..e9b60e0 100644
--- a/src/shared/store/user.ts
+++ b/src/shared/store/user.ts
@@ -13,6 +13,9 @@ export const useUserStore = defineStore("user", () => {
user.value?.admin_type === USER_TYPE.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,
)
@@ -37,6 +40,7 @@ export const useUserStore = defineStore("user", () => {
isFinished,
user,
isAdminRole,
+ isTheAdmin,
isSuperAdmin,
hasProblemPermission,
isAuthed,
diff --git a/src/utils/functions.ts b/src/utils/functions.ts
index 5ea74aa..dbfbfc2 100644
--- a/src/utils/functions.ts
+++ b/src/utils/functions.ts
@@ -1,34 +1,30 @@
-import { getTime, intervalToDuration, parseISO } from "date-fns"
+import { getTime, intervalToDuration, parseISO, type Duration } from "date-fns"
import { User } from "./types"
+import { USER_TYPE } from "./constants"
-export function getACRate(acCount: number, totalCount: number) {
- let rate = ""
- if (totalCount === 0) rate = "0.00"
- else {
- if (acCount >= totalCount) rate = "100.00"
- else rate = ((acCount / totalCount) * 100).toFixed(2)
- }
- return `${rate}%`
+function calculateACRate(acCount: number, totalCount: number): string {
+ if (totalCount === 0) return "0.00"
+ if (acCount >= totalCount) return "100.00"
+ return ((acCount / totalCount) * 100).toFixed(2)
}
-export function getACRateNumber(acCount: number, totalCount: number) {
- let rate = ""
- if (totalCount === 0) rate = "0.00"
- else {
- if (acCount >= totalCount) rate = "100.00"
- else rate = ((acCount / totalCount) * 100).toFixed(2)
- }
- return parseFloat(rate)
+export function getACRate(acCount: number, totalCount: number): string {
+ return `${calculateACRate(acCount, totalCount)}%`
}
-export function filterEmptyValue(object: any) {
- let query: any = {}
- Object.keys(object).forEach((key) => {
- if (object[key] || object[key] === 0 || object[key] === false) {
- query[key] = object[key]
+export function getACRateNumber(acCount: number, totalCount: number): number {
+ return parseFloat(calculateACRate(acCount, totalCount))
+}
+
+export function filterEmptyValue>(
+ object: T,
+): Partial {
+ return Object.entries(object).reduce((query, [key, value]) => {
+ if (value != null && value !== "" && value !== undefined) {
+ query[key as keyof T] = value
}
- })
- return query
+ return query
+ }, {} as Partial)
}
export function getTagColor(
@@ -50,56 +46,52 @@ export function parseTime(utc: Date | string, format = "YYYY年M月D日") {
return time.value
}
+function getDurationObject(start: Date | string, end: Date | string) {
+ return intervalToDuration({
+ start: getTime(parseISO(start.toString())),
+ end: getTime(parseISO(end.toString())),
+ })
+}
+
+function formatDurationUnits(
+ duration: Duration,
+ units: Array<{ key: keyof Duration; suffix: string }>,
+): string {
+ return units
+ .filter(({ key }) => duration[key])
+ .map(({ key, suffix }) => duration[key] + suffix)
+ .join("")
+}
+
export function duration(
start: Date | string,
end: Date | string,
showSeconds = false,
): string {
- const duration = intervalToDuration({
- start: getTime(parseISO(start.toString())),
- end: getTime(parseISO(end.toString())),
- })
- let result = ""
- if (duration.years) {
- result += duration.years + "年"
- }
- if (duration.months) {
- result += duration.months + "月"
- }
- if (duration.days) {
- result += duration.days + "天"
- }
- if (duration.hours) {
- result += duration.hours + "小时"
- }
- if (duration.minutes) {
- result += duration.minutes + "分钟"
- }
- if (showSeconds && duration.seconds) {
- result += duration.seconds + "秒"
- }
- return result
+ const durationObj = getDurationObject(start, end)
+ const units = [
+ { key: "years" as const, suffix: "年" },
+ { key: "months" as const, suffix: "月" },
+ { key: "days" as const, suffix: "天" },
+ { key: "hours" as const, suffix: "小时" },
+ { key: "minutes" as const, suffix: "分钟" },
+ ...(showSeconds ? [{ key: "seconds" as const, suffix: "秒" }] : []),
+ ]
+ return formatDurationUnits(durationObj, units)
}
export function durationToDays(
start: Date | string,
end: Date | string,
): string {
- const duration = intervalToDuration({
- start: getTime(parseISO(start.toString())),
- end: getTime(parseISO(end.toString())),
- })
- let result = ""
- if (duration.years) {
- result += duration.years + "年"
- }
- if (duration.months) {
- result += duration.months + "月"
- }
- if (duration.days) {
- result += duration.days + "天"
- }
- return !!result ? result : "一天以内"
+ const durationObj = getDurationObject(start, end)
+ const units = [
+ { key: "years" as const, suffix: "年" },
+ { key: "months" as const, suffix: "月" },
+ { key: "days" as const, suffix: "天" },
+ ]
+ const result = formatDurationUnits(durationObj, units)
+ return result || "一天以内"
}
export function secondsToDuration(seconds: number): string {
@@ -126,13 +118,14 @@ export function submissionTimeFormat(time: number | string | undefined) {
return time + "ms"
}
-export function debounce(fn: Function, n = 100) {
- let handle: any
- return (...args: any[]) => {
- if (handle) clearTimeout(handle)
- handle = setTimeout(() => {
- fn(...args)
- }, n)
+export function debounce any>(
+ fn: T,
+ delay = 100,
+): (...args: Parameters) => void {
+ let timeoutId: ReturnType
+ return (...args: Parameters) => {
+ clearTimeout(timeoutId)
+ timeoutId = setTimeout(() => fn(...args), delay)
}
}
@@ -140,50 +133,50 @@ export function getUserRole(role: User["admin_type"]): {
type: "default" | "info" | "error"
tagString: "普通" | "管理员" | "超管"
} {
- const obj: {
- type: "default" | "info" | "error"
- tagString: "普通" | "管理员" | "超管"
- } = { type: "default", tagString: "普通" }
- switch (role) {
- case "Regular User":
- obj.type = "default"
- obj.tagString = "普通"
- break
- case "Admin":
- obj.type = "info"
- obj.tagString = "管理员"
- break
- case "Super Admin":
- obj.type = "error"
- obj.tagString = "超管"
- break
+ const roleMap = {
+ [USER_TYPE.REGULAR_USER]: {
+ type: "default" as const,
+ tagString: "普通" as const,
+ },
+ [USER_TYPE.ADMIN]: { type: "info" as const, tagString: "管理员" as const },
+ [USER_TYPE.SUPER_ADMIN]: {
+ type: "error" as const,
+ tagString: "超管" as const,
+ },
}
- return obj
+
+ return roleMap[role] || roleMap[USER_TYPE.REGULAR_USER]
}
-export function unique(arr: T[]) {
- return arr.reduce((prev: T[], curr: T) => {
- if (!prev.includes(curr)) {
- prev.push(curr)
- }
- return prev
- }, [])
+export function unique(arr: T[]): T[] {
+ return [...new Set(arr)]
}
-export function encode(string?: string) {
- return btoa(String.fromCharCode(...new TextEncoder().encode(string ?? "")))
+export function encode(string?: string): string {
+ try {
+ return btoa(String.fromCharCode(...new TextEncoder().encode(string ?? "")))
+ } catch (error) {
+ console.error("编码失败:", error)
+ return ""
+ }
}
-export function decode(bytes?: string) {
- const latin = atob(bytes ?? "")
- return new TextDecoder("utf-8").decode(
- Uint8Array.from({ length: latin.length }, (_, index) =>
- latin.charCodeAt(index),
- ),
- )
+export function decode(bytes?: string): string {
+ try {
+ if (!bytes) return ""
+ const latin = atob(bytes)
+ return new TextDecoder("utf-8").decode(
+ Uint8Array.from({ length: latin.length }, (_, index) =>
+ latin.charCodeAt(index),
+ ),
+ )
+ } catch (error) {
+ console.error("解码失败:", error)
+ return ""
+ }
}
-export function getCSRFToken() {
+export function getCSRFToken(): string {
if (typeof document === "undefined") {
return ""
}
diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts
new file mode 100644
index 0000000..f0cc2cc
--- /dev/null
+++ b/src/utils/permissions.ts
@@ -0,0 +1,113 @@
+import { useUserStore } from "~/shared/store/user"
+
+/**
+ * 权限检查工具函数
+ */
+export function usePermissions() {
+ const userStore = useUserStore()
+
+ return {
+ // 基本权限检查
+ isAuthenticated: computed(() => userStore.isAuthed),
+ isAdminRole: computed(() => userStore.isAdminRole),
+ isSuperAdmin: computed(() => userStore.isSuperAdmin),
+ hasProblemPermission: computed(() => userStore.hasProblemPermission),
+
+ // 功能权限检查
+ canManageUsers: computed(() => userStore.isSuperAdmin),
+ canManageAnnouncements: computed(() => userStore.isSuperAdmin),
+ canManageComments: computed(() => userStore.isSuperAdmin),
+ canManageTutorials: computed(() => userStore.isSuperAdmin),
+ canManageSystemConfig: computed(() => userStore.isSuperAdmin),
+ canSendMessages: computed(() => userStore.isSuperAdmin),
+
+ canManageProblems: computed(() => userStore.hasProblemPermission),
+ canManageContests: computed(() => userStore.isSuperAdmin),
+
+ // 题目权限细分检查
+ canManageAllProblems: computed(
+ () =>
+ userStore.user?.problem_permission === "All" || userStore.isSuperAdmin,
+ ),
+ canManageOwnProblems: computed(
+ () =>
+ userStore.user?.problem_permission === "Own" && !userStore.isSuperAdmin,
+ ),
+
+ // 获取用户权限级别描述
+ getUserPermissionLevel: computed(() => {
+ if (userStore.isSuperAdmin) return "超级管理员"
+ if (userStore.isAdminRole) return "管理员"
+ return "普通用户"
+ }),
+
+ // 获取题目权限描述
+ getProblemPermissionLevel: computed(() => {
+ if (!userStore.user) return "无权限"
+
+ switch (userStore.user.problem_permission) {
+ case "All":
+ return "管理所有题目"
+ case "Own":
+ return "管理自己的题目"
+ case "None":
+ return "无题目权限"
+ default:
+ return "无权限"
+ }
+ }),
+ }
+}
+
+/**
+ * 路由权限检查
+ */
+export function checkRoutePermission(routeName: string): boolean {
+ const userStore = useUserStore()
+
+ // 需要super admin权限的路由
+ const superAdminRoutes = [
+ "admin home",
+ "admin config",
+ "admin user list",
+ "admin user generate",
+ "admin announcement list",
+ "admin announcement create",
+ "admin announcement edit",
+ "admin comment list",
+ "admin message list",
+ "admin tutorial list",
+ "admin tutorial create",
+ "admin tutorial edit",
+ "admin contest list",
+ "admin contest create",
+ "admin contest edit",
+ "admin contest problem list",
+ "admin contest problem create",
+ "admin contest problem edit",
+ ]
+
+ // 需要题目权限的路由
+ 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 (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 fcfac3b..000f9d4 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -1,4 +1,4 @@
-import { ContestStatus, ContestType } from "./constants"
+import { ContestStatus, ContestType, USER_TYPE } from "./constants"
export interface Profile {
id: number
@@ -33,12 +33,14 @@ export interface Profile {
submission_number: number
}
+export type UserAdminType = "Regular User" | "Admin" | "Super Admin"
+
export interface User {
id: number
username: string
real_name: string
email: string
- admin_type: "Regular User" | "Super Admin" | "Admin"
+ admin_type: UserAdminType
problem_permission: string
create_time: Date
last_login: Date