123
docs/PERMISSIONS.md
Normal file
123
docs/PERMISSIONS.md
Normal file
@@ -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
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePermissions } from "~/utils/permissions"
|
||||||
|
|
||||||
|
const { canManageUsers, isSuperAdmin } = usePermissions()
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
if (!canManageUsers.value) {
|
||||||
|
message.error("您没有权限访问此页面")
|
||||||
|
router.push("/admin")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 根据权限显示不同内容 -->
|
||||||
|
<n-button v-if="isSuperAdmin" type="primary">
|
||||||
|
超级管理员专用功能
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **前端权限控制只是UI层面的限制**,真正的权限控制在后端
|
||||||
|
2. **所有API调用都会在后端进行权限验证**
|
||||||
|
3. **权限信息来自后端用户接口**,前端不应该缓存或修改权限信息
|
||||||
|
4. **路由权限检查是异步的**,需要等待用户信息加载完成
|
||||||
@@ -84,11 +84,11 @@ export function getContestProblem(id: number) {
|
|||||||
export function getUserList(
|
export function getUserList(
|
||||||
offset = 0,
|
offset = 0,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
admin = "0",
|
type = "",
|
||||||
keyword: string,
|
keyword: string,
|
||||||
) {
|
) {
|
||||||
return http.get("admin/user", {
|
return http.get("admin/user", {
|
||||||
params: { paging: true, offset, limit, keyword, admin },
|
params: { paging: true, offset, limit, keyword, type },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { NButton, NTag } from "naive-ui"
|
import { NButton, NTag } from "naive-ui"
|
||||||
import { parseTime } from "~/utils/functions"
|
import { parseTime } from "~/utils/functions"
|
||||||
import { Server } from "~/utils/types"
|
import { Server } from "~/utils/types"
|
||||||
|
import { usePermissions } from "~/utils/permissions"
|
||||||
import {
|
import {
|
||||||
deleteJudgeServer,
|
deleteJudgeServer,
|
||||||
editWebsite,
|
editWebsite,
|
||||||
@@ -17,6 +18,14 @@ interface Testcase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const router = useRouter()
|
||||||
|
const { canManageSystemConfig } = usePermissions()
|
||||||
|
|
||||||
|
// 权限检查:只有super_admin可以管理系统配置
|
||||||
|
if (!canManageSystemConfig.value) {
|
||||||
|
message.error("您没有权限访问此页面")
|
||||||
|
router.push("/admin")
|
||||||
|
}
|
||||||
|
|
||||||
const testcaseColumns: DataTableColumn<Testcase>[] = [
|
const testcaseColumns: DataTableColumn<Testcase>[] = [
|
||||||
{ title: "测试用例 ID", key: "id" },
|
{ title: "测试用例 ID", key: "id" },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NButton } from "naive-ui"
|
|||||||
import { getRank } from "oj/api"
|
import { getRank } from "oj/api"
|
||||||
import Pagination from "~/shared/components/Pagination.vue"
|
import Pagination from "~/shared/components/Pagination.vue"
|
||||||
import { useUserStore } from "~/shared/store/user"
|
import { useUserStore } from "~/shared/store/user"
|
||||||
|
import { usePermissions } from "~/utils/permissions"
|
||||||
import { getACRate } from "~/utils/functions"
|
import { getACRate } from "~/utils/functions"
|
||||||
import { Rank } from "~/utils/types"
|
import { Rank } from "~/utils/types"
|
||||||
import { getBaseInfo, randomUser10 } from "../api"
|
import { getBaseInfo, randomUser10 } from "../api"
|
||||||
@@ -13,6 +14,13 @@ const contestCount = ref(0)
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const { isSuperAdmin } = usePermissions()
|
||||||
|
|
||||||
|
// 权限检查:只有super_admin可以访问管理员首页
|
||||||
|
if (!isSuperAdmin.value) {
|
||||||
|
message.error("您没有权限访问此页面")
|
||||||
|
router.push("/admin/problem/list")
|
||||||
|
}
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const luckyGuy = ref("")
|
const luckyGuy = ref("")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { USER_TYPE } from "~/utils/constants"
|
||||||
import { getUserRole } from "~/utils/functions"
|
import { getUserRole } from "~/utils/functions"
|
||||||
import { User } from "~/utils/types"
|
import { User } from "~/utils/types"
|
||||||
|
|
||||||
@@ -6,7 +7,9 @@ interface Props {
|
|||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const isAdmin = computed(() => props.user.admin_type !== "Regular User")
|
const isNotRegularUser = computed(
|
||||||
|
() => props.user.admin_type !== USER_TYPE.REGULAR_USER,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
@@ -14,7 +17,7 @@ const isAdmin = computed(() => props.user.admin_type !== "Regular User")
|
|||||||
封号中
|
封号中
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag
|
<n-tag
|
||||||
v-if="isAdmin"
|
v-if="isNotRegularUser"
|
||||||
:type="getUserRole(props.user.admin_type).type"
|
:type="getUserRole(props.user.admin_type).type"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DataTableRowKey, SelectOption } from "naive-ui"
|
import { DataTableRowKey, SelectOption } from "naive-ui"
|
||||||
import Pagination from "~/shared/components/Pagination.vue"
|
import Pagination from "~/shared/components/Pagination.vue"
|
||||||
import { parseTime } from "~/utils/functions"
|
import { parseTime, filterEmptyValue } from "~/utils/functions"
|
||||||
import { User } from "~/utils/types"
|
import { User } from "~/utils/types"
|
||||||
|
import { usePermissions } from "~/utils/permissions"
|
||||||
import {
|
import {
|
||||||
deleteUsers,
|
deleteUsers,
|
||||||
editUser,
|
editUser,
|
||||||
@@ -15,6 +16,15 @@ import Name from "./components/Name.vue"
|
|||||||
import { USER_TYPE } from "~/utils/constants"
|
import { USER_TYPE } from "~/utils/constants"
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const { canManageUsers } = usePermissions()
|
||||||
|
|
||||||
|
// 权限检查:只有super_admin可以管理用户
|
||||||
|
if (!canManageUsers.value) {
|
||||||
|
message.error("您没有权限访问此页面")
|
||||||
|
router.push("/admin")
|
||||||
|
}
|
||||||
|
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const users = ref<User[]>([])
|
const users = ref<User[]>([])
|
||||||
@@ -23,8 +33,14 @@ const query = reactive({
|
|||||||
limit: 10,
|
limit: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
keyword: "",
|
keyword: "",
|
||||||
admin: false,
|
type: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const adminOptions = [
|
||||||
|
{ label: "全部用户", value: "" },
|
||||||
|
{ label: "管理员", value: USER_TYPE.ADMIN },
|
||||||
|
{ label: "超级管理员", value: USER_TYPE.SUPER_ADMIN },
|
||||||
|
]
|
||||||
const [create, toggleCreate] = useToggle(false)
|
const [create, toggleCreate] = useToggle(false)
|
||||||
const password = ref("")
|
const password = ref("")
|
||||||
const userIDs = ref<DataTableRowKey[]>([])
|
const userIDs = ref<DataTableRowKey[]>([])
|
||||||
@@ -78,15 +94,27 @@ const columns: DataTableColumn<User>[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const options: SelectOption[] = [
|
const options: SelectOption[] = [
|
||||||
{ label: "普通", value: "Regular User" },
|
{ label: "普通", value: USER_TYPE.REGULAR_USER },
|
||||||
{ label: "管理员", value: "Admin" },
|
{ label: "管理员", value: USER_TYPE.ADMIN },
|
||||||
{ label: "超级管理员", value: "Super Admin" },
|
{ label: "超级管理员", value: USER_TYPE.SUPER_ADMIN },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function routerPush() {
|
||||||
|
router.push({
|
||||||
|
path: route.path,
|
||||||
|
query: filterEmptyValue(query),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function listUsers() {
|
async function listUsers() {
|
||||||
|
query.keyword = <string>route.query.keyword ?? ""
|
||||||
|
query.page = parseInt(<string>route.query.page) || 1
|
||||||
|
query.limit = parseInt(<string>route.query.limit) || 10
|
||||||
|
query.type = <string>route.query.type ?? ""
|
||||||
|
|
||||||
|
if (query.page < 1) query.page = 1
|
||||||
const offset = (query.page - 1) * query.limit
|
const offset = (query.page - 1) * query.limit
|
||||||
const isAdmin = query.admin ? "1" : "0"
|
const res = await getUserList(offset, query.limit, query.type, query.keyword)
|
||||||
const res = await getUserList(offset, query.limit, isAdmin, query.keyword)
|
|
||||||
total.value = res.data.total
|
total.value = res.data.total
|
||||||
users.value = res.data.results
|
users.value = res.data.results
|
||||||
}
|
}
|
||||||
@@ -127,7 +155,7 @@ function createNewUser() {
|
|||||||
username: "",
|
username: "",
|
||||||
real_name: "",
|
real_name: "",
|
||||||
email: "",
|
email: "",
|
||||||
admin_type: "Regular User",
|
admin_type: "Super Admin",
|
||||||
problem_permission: "",
|
problem_permission: "",
|
||||||
create_time: new Date(),
|
create_time: new Date(),
|
||||||
last_login: new Date(),
|
last_login: new Date(),
|
||||||
@@ -177,7 +205,28 @@ async function handleEditUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(listUsers)
|
onMounted(listUsers)
|
||||||
watch(query, listUsers, { deep: true })
|
watch(() => query.page, routerPush)
|
||||||
|
watch(
|
||||||
|
() => [query.limit, query.keyword, query.type],
|
||||||
|
() => {
|
||||||
|
query.page = 1
|
||||||
|
routerPush()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
watchDebounced(
|
||||||
|
() => query.keyword,
|
||||||
|
() => {
|
||||||
|
query.page = 1
|
||||||
|
routerPush()
|
||||||
|
},
|
||||||
|
{ debounce: 500, maxWait: 1000 },
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => route.name === "admin user list" && route.query,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) listUsers()
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -200,8 +249,12 @@ watch(query, listUsers, { deep: true })
|
|||||||
确定删除选中的用户吗?删除后无法恢复!
|
确定删除选中的用户吗?删除后无法恢复!
|
||||||
</n-popconfirm>
|
</n-popconfirm>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<span>超管出列</span>
|
<n-select
|
||||||
<n-switch v-model:value="query.admin" />
|
v-model:value="query.type"
|
||||||
|
:options="adminOptions"
|
||||||
|
placeholder="选择用户类型"
|
||||||
|
style="width: 120px"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<n-input style="width: 200px" v-model:value="query.keyword" />
|
<n-input style="width: 200px" v-model:value="query.keyword" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
61
src/main.ts
61
src/main.ts
@@ -31,17 +31,68 @@ const router = createRouter({
|
|||||||
routes: [ojs, admins],
|
routes: [ojs, admins],
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
// 检查是否需要认证
|
||||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||||
if (!storage.get(STORAGE_KEY.AUTHED)) {
|
if (!storage.get(STORAGE_KEY.AUTHED)) {
|
||||||
toggleLogin(true)
|
toggleLogin(true)
|
||||||
next("/")
|
next("/")
|
||||||
} else {
|
return
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
} 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(
|
ChartJS.register(
|
||||||
|
|||||||
@@ -70,16 +70,22 @@ async function listSubmissions() {
|
|||||||
|
|
||||||
if (query.page < 1) query.page = 1
|
if (query.page < 1) query.page = 1
|
||||||
const offset = query.limit * (query.page - 1)
|
const offset = query.limit * (query.page - 1)
|
||||||
const res = await getSubmissions({
|
try {
|
||||||
...query,
|
const res = await getSubmissions({
|
||||||
myself: query.myself ? "1" : "0",
|
...query,
|
||||||
offset,
|
myself: query.myself ? "1" : "0",
|
||||||
problem_id: <string>route.query.problem ?? "",
|
offset,
|
||||||
contest_id: <string>route.params.contestID ?? "",
|
problem_id: <string>route.query.problem ?? "",
|
||||||
language: query.language,
|
contest_id: <string>route.params.contestID ?? "",
|
||||||
})
|
language: query.language,
|
||||||
submissions.value = res.data.results
|
})
|
||||||
total.value = res.data.total
|
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() {
|
async function getTodayCount() {
|
||||||
|
|||||||
@@ -107,113 +107,138 @@ export const admins: RouteRecordRaw = {
|
|||||||
path: "",
|
path: "",
|
||||||
name: "admin home",
|
name: "admin home",
|
||||||
component: () => import("~/admin/setting/home.vue"),
|
component: () => import("~/admin/setting/home.vue"),
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
|
// 只有super_admin可以访问的路由
|
||||||
{
|
{
|
||||||
path: "config",
|
path: "config",
|
||||||
name: "admin config",
|
name: "admin config",
|
||||||
component: () => import("admin/setting/config.vue"),
|
component: () => import("admin/setting/config.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "user/list",
|
path: "user/list",
|
||||||
name: "admin user list",
|
name: "admin user list",
|
||||||
component: () => import("admin/user/list.vue"),
|
component: () => import("admin/user/list.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "user/generate",
|
path: "user/generate",
|
||||||
name: "admin user generate",
|
name: "admin user generate",
|
||||||
component: () => import("~/admin/user/generate.vue"),
|
component: () => import("~/admin/user/generate.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
|
// admin和super_admin都可以访问的路由 (需要题目权限)
|
||||||
{
|
{
|
||||||
path: "problem/list",
|
path: "problem/list",
|
||||||
name: "admin problem list",
|
name: "admin problem list",
|
||||||
component: () => import("admin/problem/list.vue"),
|
component: () => import("admin/problem/list.vue"),
|
||||||
|
meta: { requiresProblemPermission: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "problem/create",
|
path: "problem/create",
|
||||||
name: "admin problem create",
|
name: "admin problem create",
|
||||||
component: () => import("admin/problem/detail.vue"),
|
component: () => import("admin/problem/detail.vue"),
|
||||||
|
meta: { requiresProblemPermission: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "problem/edit/:problemID",
|
path: "problem/edit/:problemID",
|
||||||
name: "admin problem edit",
|
name: "admin problem edit",
|
||||||
component: () => import("admin/problem/detail.vue"),
|
component: () => import("admin/problem/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { requiresProblemPermission: true },
|
||||||
},
|
},
|
||||||
|
// admin和super_admin都可以访问的路由
|
||||||
{
|
{
|
||||||
path: "contest/list",
|
path: "contest/list",
|
||||||
name: "admin contest list",
|
name: "admin contest list",
|
||||||
component: () => import("admin/contest/list.vue"),
|
component: () => import("admin/contest/list.vue"),
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/create",
|
path: "contest/create",
|
||||||
name: "admin contest create",
|
name: "admin contest create",
|
||||||
component: () => import("admin/contest/detail.vue"),
|
component: () => import("admin/contest/detail.vue"),
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/edit/:contestID",
|
path: "contest/edit/:contestID",
|
||||||
name: "admin contest edit",
|
name: "admin contest edit",
|
||||||
component: () => import("admin/contest/detail.vue"),
|
component: () => import("admin/contest/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/:contestID/problem/list",
|
path: "contest/:contestID/problem/list",
|
||||||
name: "admin contest problem list",
|
name: "admin contest problem list",
|
||||||
component: () => import("admin/problem/list.vue"),
|
component: () => import("admin/problem/list.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/:contestID/problem/create",
|
path: "contest/:contestID/problem/create",
|
||||||
name: "admin contest problem create",
|
name: "admin contest problem create",
|
||||||
component: () => import("admin/problem/detail.vue"),
|
component: () => import("admin/problem/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/:contestID/problem/edit/:problemID",
|
path: "contest/:contestID/problem/edit/:problemID",
|
||||||
name: "admin contest problem edit",
|
name: "admin contest problem edit",
|
||||||
component: () => import("admin/problem/detail.vue"),
|
component: () => import("admin/problem/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
|
// 只有super_admin可以访问的路由
|
||||||
{
|
{
|
||||||
path: "announcement/list",
|
path: "announcement/list",
|
||||||
name: "admin announcement list",
|
name: "admin announcement list",
|
||||||
component: () => import("admin/announcement/list.vue"),
|
component: () => import("admin/announcement/list.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "announcement/create",
|
path: "announcement/create",
|
||||||
name: "admin announcement create",
|
name: "admin announcement create",
|
||||||
component: () => import("admin/announcement/detail.vue"),
|
component: () => import("admin/announcement/detail.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "announcement/edit/:announcementID",
|
path: "announcement/edit/:announcementID",
|
||||||
name: "admin announcement edit",
|
name: "admin announcement edit",
|
||||||
component: () => import("admin/announcement/detail.vue"),
|
component: () => import("admin/announcement/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "comment/list",
|
path: "comment/list",
|
||||||
name: "admin comment list",
|
name: "admin comment list",
|
||||||
component: () => import("admin/communication/comments.vue"),
|
component: () => import("admin/communication/comments.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "message/list",
|
path: "message/list",
|
||||||
name: "admin message list",
|
name: "admin message list",
|
||||||
component: () => import("admin/communication/messages.vue"),
|
component: () => import("admin/communication/messages.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "tutorial/list",
|
path: "tutorial/list",
|
||||||
name: "admin tutorial list",
|
name: "admin tutorial list",
|
||||||
component: () => import("admin/tutorial/list.vue"),
|
component: () => import("admin/tutorial/list.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "tutorial/create",
|
path: "tutorial/create",
|
||||||
name: "admin tutorial create",
|
name: "admin tutorial create",
|
||||||
component: () => import("admin/tutorial/detail.vue"),
|
component: () => import("admin/tutorial/detail.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "tutorial/edit/:tutorialID",
|
path: "tutorial/edit/:tutorialID",
|
||||||
name: "admin tutorial edit",
|
name: "admin tutorial edit",
|
||||||
component: () => import("admin/tutorial/detail.vue"),
|
component: () => import("admin/tutorial/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ const menus = computed<MenuOption[]>(() => [
|
|||||||
icon: renderIcon("streamline-emojis:palm-tree"),
|
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,
|
show: userStore.isAdminRole,
|
||||||
key: "admin",
|
key: "admin",
|
||||||
icon: renderIcon("streamline-emojis:ghost"),
|
icon: renderIcon("streamline-emojis:ghost"),
|
||||||
|
|||||||
@@ -7,55 +7,92 @@ import { useUserStore } from "../store/user"
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const options: MenuOption[] = [
|
|
||||||
{
|
// 根据用户权限动态生成菜单选项
|
||||||
label: () => h(RouterLink, { to: "/" }, { default: () => "前台" }),
|
const options = computed<MenuOption[]>(() => {
|
||||||
key: "return to OJ",
|
const baseOptions: MenuOption[] = [
|
||||||
},
|
{
|
||||||
{
|
label: () => h(RouterLink, { to: "/" }, { default: () => "前台" }),
|
||||||
label: () => h(RouterLink, { to: "/admin" }, { default: () => "管理" }),
|
key: "return to OJ",
|
||||||
key: "admin home",
|
},
|
||||||
},
|
]
|
||||||
{
|
|
||||||
label: () =>
|
// admin 可以访问的功能
|
||||||
h(RouterLink, { to: "/admin/config" }, { default: () => "设置" }),
|
if (userStore.isTheAdmin) {
|
||||||
key: "admin config",
|
baseOptions.push({
|
||||||
},
|
label: () =>
|
||||||
{
|
h(RouterLink, { to: "/admin/problem/list" }, { default: () => "题目" }),
|
||||||
label: () =>
|
key: "admin problem list",
|
||||||
h(RouterLink, { to: "/admin/problem/list" }, { default: () => "题目" }),
|
})
|
||||||
key: "admin problem list",
|
}
|
||||||
},
|
|
||||||
{
|
// super_admin 可以访问的功能
|
||||||
label: () =>
|
if (userStore.isSuperAdmin) {
|
||||||
h(RouterLink, { to: "/admin/comment/list" }, { default: () => "评论" }),
|
baseOptions.push(
|
||||||
key: "admin comment list",
|
{
|
||||||
},
|
label: () => h(RouterLink, { to: "/admin" }, { default: () => "管理" }),
|
||||||
{
|
key: "admin home",
|
||||||
label: () =>
|
},
|
||||||
h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户" }),
|
{
|
||||||
key: "admin user list",
|
label: () =>
|
||||||
},
|
h(
|
||||||
{
|
RouterLink,
|
||||||
label: () =>
|
{ to: "/admin/problem/list" },
|
||||||
h(RouterLink, { to: "/admin/contest/list" }, { default: () => "比赛" }),
|
{ default: () => "题目" },
|
||||||
key: "admin contest list",
|
),
|
||||||
},
|
key: "admin problem list",
|
||||||
{
|
},
|
||||||
label: () =>
|
{
|
||||||
h(
|
label: () =>
|
||||||
RouterLink,
|
h(
|
||||||
{ to: "/admin/announcement/list" },
|
RouterLink,
|
||||||
{ default: () => "公告" },
|
{ to: "/admin/contest/list" },
|
||||||
),
|
{ default: () => "比赛" },
|
||||||
key: "admin announcement list",
|
),
|
||||||
},
|
key: "admin contest list",
|
||||||
{
|
},
|
||||||
label: () =>
|
{
|
||||||
h(RouterLink, { to: "/admin/tutorial/list" }, { default: () => "教程" }),
|
label: () =>
|
||||||
key: "admin tutorial list",
|
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")
|
const active = computed(() => (route.name as string) || "home")
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
user.value?.admin_type === USER_TYPE.ADMIN ||
|
user.value?.admin_type === USER_TYPE.ADMIN ||
|
||||||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
|
user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
|
||||||
)
|
)
|
||||||
|
const isTheAdmin = computed(
|
||||||
|
() => user.value?.admin_type === USER_TYPE.ADMIN,
|
||||||
|
)
|
||||||
const isSuperAdmin = computed(
|
const isSuperAdmin = computed(
|
||||||
() => user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
|
() => user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
|
||||||
)
|
)
|
||||||
@@ -37,6 +40,7 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
isFinished,
|
isFinished,
|
||||||
user,
|
user,
|
||||||
isAdminRole,
|
isAdminRole,
|
||||||
|
isTheAdmin,
|
||||||
isSuperAdmin,
|
isSuperAdmin,
|
||||||
hasProblemPermission,
|
hasProblemPermission,
|
||||||
isAuthed,
|
isAuthed,
|
||||||
|
|||||||
@@ -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 } from "./types"
|
||||||
|
import { USER_TYPE } from "./constants"
|
||||||
|
|
||||||
export function getACRate(acCount: number, totalCount: number) {
|
function calculateACRate(acCount: number, totalCount: number): string {
|
||||||
let rate = ""
|
if (totalCount === 0) return "0.00"
|
||||||
if (totalCount === 0) rate = "0.00"
|
if (acCount >= totalCount) return "100.00"
|
||||||
else {
|
return ((acCount / totalCount) * 100).toFixed(2)
|
||||||
if (acCount >= totalCount) rate = "100.00"
|
|
||||||
else rate = ((acCount / totalCount) * 100).toFixed(2)
|
|
||||||
}
|
|
||||||
return `${rate}%`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getACRateNumber(acCount: number, totalCount: number) {
|
export function getACRate(acCount: number, totalCount: number): string {
|
||||||
let rate = ""
|
return `${calculateACRate(acCount, totalCount)}%`
|
||||||
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 filterEmptyValue(object: any) {
|
export function getACRateNumber(acCount: number, totalCount: number): number {
|
||||||
let query: any = {}
|
return parseFloat(calculateACRate(acCount, totalCount))
|
||||||
Object.keys(object).forEach((key) => {
|
}
|
||||||
if (object[key] || object[key] === 0 || object[key] === false) {
|
|
||||||
query[key] = object[key]
|
export function filterEmptyValue<T extends Record<string, any>>(
|
||||||
|
object: T,
|
||||||
|
): Partial<T> {
|
||||||
|
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<T>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTagColor(
|
export function getTagColor(
|
||||||
@@ -50,56 +46,52 @@ export function parseTime(utc: Date | string, format = "YYYY年M月D日") {
|
|||||||
return time.value
|
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(
|
export function duration(
|
||||||
start: Date | string,
|
start: Date | string,
|
||||||
end: Date | string,
|
end: Date | string,
|
||||||
showSeconds = false,
|
showSeconds = false,
|
||||||
): string {
|
): string {
|
||||||
const duration = intervalToDuration({
|
const durationObj = getDurationObject(start, end)
|
||||||
start: getTime(parseISO(start.toString())),
|
const units = [
|
||||||
end: getTime(parseISO(end.toString())),
|
{ key: "years" as const, suffix: "年" },
|
||||||
})
|
{ key: "months" as const, suffix: "月" },
|
||||||
let result = ""
|
{ key: "days" as const, suffix: "天" },
|
||||||
if (duration.years) {
|
{ key: "hours" as const, suffix: "小时" },
|
||||||
result += duration.years + "年"
|
{ key: "minutes" as const, suffix: "分钟" },
|
||||||
}
|
...(showSeconds ? [{ key: "seconds" as const, suffix: "秒" }] : []),
|
||||||
if (duration.months) {
|
]
|
||||||
result += duration.months + "月"
|
return formatDurationUnits(durationObj, units)
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function durationToDays(
|
export function durationToDays(
|
||||||
start: Date | string,
|
start: Date | string,
|
||||||
end: Date | string,
|
end: Date | string,
|
||||||
): string {
|
): string {
|
||||||
const duration = intervalToDuration({
|
const durationObj = getDurationObject(start, end)
|
||||||
start: getTime(parseISO(start.toString())),
|
const units = [
|
||||||
end: getTime(parseISO(end.toString())),
|
{ key: "years" as const, suffix: "年" },
|
||||||
})
|
{ key: "months" as const, suffix: "月" },
|
||||||
let result = ""
|
{ key: "days" as const, suffix: "天" },
|
||||||
if (duration.years) {
|
]
|
||||||
result += duration.years + "年"
|
const result = formatDurationUnits(durationObj, units)
|
||||||
}
|
return result || "一天以内"
|
||||||
if (duration.months) {
|
|
||||||
result += duration.months + "月"
|
|
||||||
}
|
|
||||||
if (duration.days) {
|
|
||||||
result += duration.days + "天"
|
|
||||||
}
|
|
||||||
return !!result ? result : "一天以内"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function secondsToDuration(seconds: number): string {
|
export function secondsToDuration(seconds: number): string {
|
||||||
@@ -126,13 +118,14 @@ export function submissionTimeFormat(time: number | string | undefined) {
|
|||||||
return time + "ms"
|
return time + "ms"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce(fn: Function, n = 100) {
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
let handle: any
|
fn: T,
|
||||||
return (...args: any[]) => {
|
delay = 100,
|
||||||
if (handle) clearTimeout(handle)
|
): (...args: Parameters<T>) => void {
|
||||||
handle = setTimeout(() => {
|
let timeoutId: ReturnType<typeof setTimeout>
|
||||||
fn(...args)
|
return (...args: Parameters<T>) => {
|
||||||
}, n)
|
clearTimeout(timeoutId)
|
||||||
|
timeoutId = setTimeout(() => fn(...args), delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,50 +133,50 @@ export function getUserRole(role: User["admin_type"]): {
|
|||||||
type: "default" | "info" | "error"
|
type: "default" | "info" | "error"
|
||||||
tagString: "普通" | "管理员" | "超管"
|
tagString: "普通" | "管理员" | "超管"
|
||||||
} {
|
} {
|
||||||
const obj: {
|
const roleMap = {
|
||||||
type: "default" | "info" | "error"
|
[USER_TYPE.REGULAR_USER]: {
|
||||||
tagString: "普通" | "管理员" | "超管"
|
type: "default" as const,
|
||||||
} = { type: "default", tagString: "普通" }
|
tagString: "普通" as const,
|
||||||
switch (role) {
|
},
|
||||||
case "Regular User":
|
[USER_TYPE.ADMIN]: { type: "info" as const, tagString: "管理员" as const },
|
||||||
obj.type = "default"
|
[USER_TYPE.SUPER_ADMIN]: {
|
||||||
obj.tagString = "普通"
|
type: "error" as const,
|
||||||
break
|
tagString: "超管" as const,
|
||||||
case "Admin":
|
},
|
||||||
obj.type = "info"
|
|
||||||
obj.tagString = "管理员"
|
|
||||||
break
|
|
||||||
case "Super Admin":
|
|
||||||
obj.type = "error"
|
|
||||||
obj.tagString = "超管"
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
return obj
|
|
||||||
|
return roleMap[role] || roleMap[USER_TYPE.REGULAR_USER]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unique<T>(arr: T[]) {
|
export function unique<T>(arr: T[]): T[] {
|
||||||
return arr.reduce((prev: T[], curr: T) => {
|
return [...new Set(arr)]
|
||||||
if (!prev.includes(curr)) {
|
|
||||||
prev.push(curr)
|
|
||||||
}
|
|
||||||
return prev
|
|
||||||
}, [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encode(string?: string) {
|
export function encode(string?: string): string {
|
||||||
return btoa(String.fromCharCode(...new TextEncoder().encode(string ?? "")))
|
try {
|
||||||
|
return btoa(String.fromCharCode(...new TextEncoder().encode(string ?? "")))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("编码失败:", error)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decode(bytes?: string) {
|
export function decode(bytes?: string): string {
|
||||||
const latin = atob(bytes ?? "")
|
try {
|
||||||
return new TextDecoder("utf-8").decode(
|
if (!bytes) return ""
|
||||||
Uint8Array.from({ length: latin.length }, (_, index) =>
|
const latin = atob(bytes)
|
||||||
latin.charCodeAt(index),
|
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") {
|
if (typeof document === "undefined") {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/utils/permissions.ts
Normal file
113
src/utils/permissions.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ContestStatus, ContestType } from "./constants"
|
import { ContestStatus, ContestType, USER_TYPE } from "./constants"
|
||||||
|
|
||||||
export interface Profile {
|
export interface Profile {
|
||||||
id: number
|
id: number
|
||||||
@@ -33,12 +33,14 @@ export interface Profile {
|
|||||||
submission_number: number
|
submission_number: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserAdminType = "Regular User" | "Admin" | "Super Admin"
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
real_name: string
|
real_name: string
|
||||||
email: string
|
email: string
|
||||||
admin_type: "Regular User" | "Super Admin" | "Admin"
|
admin_type: UserAdminType
|
||||||
problem_permission: string
|
problem_permission: string
|
||||||
create_time: Date
|
create_time: Date
|
||||||
last_login: Date
|
last_login: Date
|
||||||
|
|||||||
Reference in New Issue
Block a user