fix contest conflict.
This commit is contained in:
@@ -1,22 +1,10 @@
|
|||||||
import http from "utils/http"
|
import http from "utils/http"
|
||||||
import { Problem } from "~/utils/types"
|
import { Problem } from "~/utils/types"
|
||||||
|
|
||||||
export async function getProblemList(
|
export async function getProblemList(offset = 0, limit = 10, keyword: string) {
|
||||||
offset = 0,
|
const res = await http.get("admin/problem", {
|
||||||
limit = 10,
|
params: { paging: true, offset, limit, keyword },
|
||||||
searchParams: any = {}
|
|
||||||
) {
|
|
||||||
let params: any = {
|
|
||||||
paging: true,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
}
|
|
||||||
Object.keys(searchParams).forEach((element) => {
|
|
||||||
if (searchParams[element]) {
|
|
||||||
params[element] = searchParams[element]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const res = await http.get("admin/problem", { params })
|
|
||||||
return {
|
return {
|
||||||
results: res.data.results.map((result: Problem) => ({
|
results: res.data.results.map((result: Problem) => ({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
@@ -41,3 +29,9 @@ export function editProblem(problem: Problem) {
|
|||||||
export function getProblem(id: number) {
|
export function getProblem(id: number) {
|
||||||
return http.get("admin/problem", { params: { id } })
|
return http.get("admin/problem", { params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUserList(offset = 0, limit = 10, keyword: string) {
|
||||||
|
return http.get("admin/user", {
|
||||||
|
params: { paging: true, offset, limit, keyword },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
48
src/admin/problem/components/Actions.vue
Normal file
48
src/admin/problem/components/Actions.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { deleteProblem } from "~/admin/api"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
problemID: number
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits(["deleted"])
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
async function handleDeleteProblem(problemID: number) {
|
||||||
|
await deleteProblem(problemID)
|
||||||
|
message.success("删除成功")
|
||||||
|
emit("deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
console.log(props.problemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goEdit() {
|
||||||
|
router.push({
|
||||||
|
name: "problem edit",
|
||||||
|
params: { problemID: props.problemID },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<n-space align="center">
|
||||||
|
<n-button size="small" secondary type="primary" @click="goEdit">
|
||||||
|
编辑
|
||||||
|
</n-button>
|
||||||
|
<n-popconfirm @positive-click="() => handleDeleteProblem(props.problemID)">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button secondary size="small" type="error">删除</n-button>
|
||||||
|
</template>
|
||||||
|
确定删除这道题目吗?相关的提交也会被相应删除哦 😯
|
||||||
|
</n-popconfirm>
|
||||||
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" secondary @click="download">下载</n-button>
|
||||||
|
</template>
|
||||||
|
下载测试用例
|
||||||
|
</n-tooltip>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { deleteProblem } from "~/admin/api"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
problemID: number
|
|
||||||
}
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits(["deleted"])
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
async function handleDeleteProblem(problemID: number) {
|
|
||||||
await deleteProblem(problemID)
|
|
||||||
message.success("删除成功")
|
|
||||||
emit("deleted")
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<n-popconfirm @positive-click="() => handleDeleteProblem(props.problemID)">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button tertiary size="small" type="error">删除</n-button>
|
|
||||||
</template>
|
|
||||||
确定删除这道题目吗?相关的提交也会被相应删除哦 😯
|
|
||||||
</n-popconfirm>
|
|
||||||
</template>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
const props = defineProps<{ problemID: number }>()
|
|
||||||
|
|
||||||
function download() {
|
|
||||||
console.log(props.problemID)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<n-tooltip>
|
|
||||||
<template #trigger>
|
|
||||||
<n-button size="small" tertiary @click="download">下载</n-button>
|
|
||||||
</template>
|
|
||||||
下载测试用例
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getProblemList, getProblem, editProblem } from "../api"
|
import { getProblemList, getProblem, editProblem } from "../api"
|
||||||
import Pagination from "~/shared/Pagination.vue"
|
import Pagination from "~/shared/Pagination.vue"
|
||||||
import { DataTableColumn, NButton, NSwitch } from "naive-ui"
|
import { DataTableColumn, NSwitch } from "naive-ui"
|
||||||
import { AdminProblemFiltered } from "~/utils/types"
|
import { AdminProblemFiltered } from "~/utils/types"
|
||||||
import { parseTime } from "~/utils/functions"
|
import { parseTime } from "~/utils/functions"
|
||||||
import DownloadTestcases from "./components/DownloadTestcases.vue"
|
import Actions from "./components/Actions.vue"
|
||||||
import DeleteProblem from "./components/DeleteProblem.vue"
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const problems = ref<AdminProblemFiltered[]>([])
|
const problems = ref<AdminProblemFiltered[]>([])
|
||||||
@@ -41,38 +38,14 @@ const columns: DataTableColumn<AdminProblemFiltered>[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "edit",
|
key: "edit",
|
||||||
render: (row) =>
|
width: 200,
|
||||||
h(
|
render: (row) => h(Actions, { problemID: row.id, onDeleted: listProblems }),
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
type: "primary",
|
|
||||||
size: "small",
|
|
||||||
tertiary: true,
|
|
||||||
onClick: () =>
|
|
||||||
router.push({
|
|
||||||
name: "problem edit",
|
|
||||||
params: { problemID: row.id },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ default: () => "编辑" }
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "delete",
|
|
||||||
render: (row) =>
|
|
||||||
h(DeleteProblem, { problemID: row.id, onDeleted: listProblems }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "download",
|
|
||||||
render: (row) => h(DownloadTestcases, { problemID: row.id }),
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
async function listProblems() {
|
async function listProblems() {
|
||||||
const offset = (query.page - 1) * query.limit
|
const offset = (query.page - 1) * query.limit
|
||||||
const res = await getProblemList(offset, query.limit, {
|
const res = await getProblemList(offset, query.limit, query.keyword)
|
||||||
keyword: query.keyword,
|
|
||||||
})
|
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
problems.value = res.results
|
problems.value = res.results
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/admin/user/components/Actions.vue
Normal file
15
src/admin/user/components/Actions.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { User } from "~/utils/types"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<n-space align="center">
|
||||||
|
<n-button size="small" secondary type="primary">编辑</n-button>
|
||||||
|
<n-button size="small" secondary type="error">封号</n-button>
|
||||||
|
<n-button size="small" secondary type="default">删除</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
25
src/admin/user/components/Name.vue
Normal file
25
src/admin/user/components/Name.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { User } from "~/utils/types"
|
||||||
|
import { getUserRole } from "~/utils/functions"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isAdmin = computed(() => props.user.admin_type !== "Regular User")
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<n-space align="center">
|
||||||
|
<n-tag v-if="props.user.is_disabled" type="error" size="small">
|
||||||
|
封号中
|
||||||
|
</n-tag>
|
||||||
|
<n-tag
|
||||||
|
v-if="isAdmin"
|
||||||
|
:type="getUserRole(props.user.admin_type).type"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getUserRole(props.user.admin_type).tagString }}
|
||||||
|
</n-tag>
|
||||||
|
{{ props.user.username }}
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,75 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { DataTableColumn } from "naive-ui"
|
||||||
|
import Pagination from "~/shared/Pagination.vue"
|
||||||
|
import { parseTime } from "~/utils/functions"
|
||||||
|
import { User } from "~/utils/types"
|
||||||
|
import { getUserList } from "../api"
|
||||||
|
import Actions from "./components/Actions.vue"
|
||||||
|
import Name from "./components/Name.vue"
|
||||||
|
|
||||||
|
const total = ref(0)
|
||||||
|
const users = ref<User[]>([])
|
||||||
|
const query = reactive({
|
||||||
|
limit: 10,
|
||||||
|
page: 1,
|
||||||
|
keyword: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns: DataTableColumn<User>[] = [
|
||||||
|
{ title: "ID", key: "id", width: 60 },
|
||||||
|
{
|
||||||
|
title: "用户名",
|
||||||
|
key: "username",
|
||||||
|
width: 150,
|
||||||
|
render: (row) => h(Name, { user: row }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "创建时间",
|
||||||
|
key: "create_time",
|
||||||
|
width: 200,
|
||||||
|
render: (row) => parseTime(row.create_time, "YYYY-MM-DD hh:mm:ss"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "上次登录",
|
||||||
|
key: "last_login",
|
||||||
|
width: 200,
|
||||||
|
render: (row) =>
|
||||||
|
row.last_login
|
||||||
|
? parseTime(row.last_login, "YYYY-MM-DD hh:mm:ss")
|
||||||
|
: "从未登录",
|
||||||
|
},
|
||||||
|
{ title: "真名", key: "real_name", width: 100 },
|
||||||
|
{ title: "邮箱", key: "email", width: 200 },
|
||||||
|
{
|
||||||
|
key: "edit",
|
||||||
|
width: 200,
|
||||||
|
render: (row) => h(Actions, { user: row }),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function listUsers() {
|
||||||
|
const offset = (query.page - 1) * query.limit
|
||||||
|
const res = await getUserList(offset, query.limit, query.keyword)
|
||||||
|
total.value = res.data.total
|
||||||
|
users.value = res.data.results
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(listUsers)
|
||||||
|
watch(query, listUsers, { deep: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>user list</div>
|
<n-form inline label-placement="left">
|
||||||
|
<n-form-item>
|
||||||
|
<n-input placeholder="请输入关键字搜索" v-model:value="query.keyword" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<n-data-table :data="users" :columns="columns" size="small" striped />
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:limit="query.limit"
|
||||||
|
v-model:page="query.page"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
34
src/components.d.ts
vendored
34
src/components.d.ts
vendored
@@ -9,31 +9,31 @@ export {}
|
|||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
IEpBell: typeof import("~icons/ep/bell")["default"]
|
IEpBell: typeof import('~icons/ep/bell')['default']
|
||||||
IEpCaretRight: typeof import("~icons/ep/caret-right")["default"]
|
IEpCaretRight: typeof import('~icons/ep/caret-right')['default']
|
||||||
IEpLoading: typeof import("~icons/ep/loading")["default"]
|
IEpLoading: typeof import('~icons/ep/loading')['default']
|
||||||
IEpLock: typeof import("~icons/ep/lock")["default"]
|
IEpLock: typeof import('~icons/ep/lock')['default']
|
||||||
IEpMenu: typeof import('~icons/ep/menu')['default']
|
IEpMenu: typeof import('~icons/ep/menu')['default']
|
||||||
IEpMoon: typeof import('~icons/ep/moon')['default']
|
IEpMoon: typeof import('~icons/ep/moon')['default']
|
||||||
IEpMoreFilled: typeof import("~icons/ep/more-filled")["default"]
|
IEpMoreFilled: typeof import('~icons/ep/more-filled')['default']
|
||||||
IEpSunny: typeof import('~icons/ep/sunny')['default']
|
IEpSunny: typeof import('~icons/ep/sunny')['default']
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
NAvatar: typeof import("naive-ui")["NAvatar"]
|
NAvatar: typeof import("naive-ui")["NAvatar"]
|
||||||
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
||||||
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NCard: typeof import("naive-ui")["NCard"]
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCode: typeof import("naive-ui")["NCode"]
|
NCode: typeof import("naive-ui")["NCode"]
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
NDescriptions: typeof import("naive-ui")["NDescriptions"]
|
NDescriptions: typeof import('naive-ui')['NDescriptions']
|
||||||
NDescriptionsItem: typeof import("naive-ui")["NDescriptionsItem"]
|
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
NEmpty: typeof import("naive-ui")["NEmpty"]
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NForm: typeof import('naive-ui')['NForm']
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
NGi: typeof import("naive-ui")["NGi"]
|
NGi: typeof import('naive-ui')['NGi']
|
||||||
NGrid: typeof import("naive-ui")["NGrid"]
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
@@ -45,14 +45,14 @@ declare module '@vue/runtime-core' {
|
|||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
NPagination: typeof import('naive-ui')['NPagination']
|
NPagination: typeof import('naive-ui')['NPagination']
|
||||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||||
NPopover: typeof import("naive-ui")["NPopover"]
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NScrollbar: typeof import("naive-ui")["NScrollbar"]
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
NSwitch: typeof import("naive-ui")["NSwitch"]
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTabPane: typeof import("naive-ui")["NTabPane"]
|
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||||
NTabs: typeof import("naive-ui")["NTabs"]
|
NTabs: typeof import('naive-ui')['NTabs']
|
||||||
NTag: typeof import("naive-ui")["NTag"]
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
NUpload: typeof import("naive-ui")["NUpload"]
|
NUpload: typeof import("naive-ui")["NUpload"]
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ function select(key: string) {
|
|||||||
<n-space>
|
<n-space>
|
||||||
<n-button @click="reset">重置</n-button>
|
<n-button @click="reset">重置</n-button>
|
||||||
<n-button @click="goSubmissions">提交信息</n-button>
|
<n-button @click="goSubmissions">提交信息</n-button>
|
||||||
<n-button v-if="userStore.isSuperAdmin" @click="edit">编辑</n-button>
|
<n-button type="warning" v-if="userStore.isSuperAdmin" @click="edit">
|
||||||
|
编辑
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserStore } from "~/shared/store/user"
|
import { useUserStore } from "~/shared/store/user"
|
||||||
import { filterEmptyValue, getTagColor, debounce } from "utils/functions"
|
import { filterEmptyValue, getTagColor } from "utils/functions"
|
||||||
import { ProblemFiltered } from "utils/types"
|
import { ProblemFiltered } from "utils/types"
|
||||||
import { isDesktop } from "~/shared/composables/breakpoints"
|
import { isDesktop } from "~/shared/composables/breakpoints"
|
||||||
import { getProblemList, getProblemTagList, getRandomProblemID } from "oj/api"
|
import { getProblemList, getProblemTagList, getRandomProblemID } from "oj/api"
|
||||||
|
|
||||||
import Pagination from "~/shared/Pagination.vue"
|
import Pagination from "~/shared/Pagination.vue"
|
||||||
import { DataTableColumn, NSpace, NTag } from "naive-ui"
|
import { DataTableColumn, NSpace, NTag } from "naive-ui"
|
||||||
import ProblemStatus from "./components/ProblemStatus.vue"
|
import ProblemStatus from "./components/ProblemStatus.vue"
|
||||||
|
|
||||||
|
interface Tag {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
checked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface Query {
|
interface Query {
|
||||||
keyword: string
|
keyword: string
|
||||||
difficulty: string
|
difficulty: string
|
||||||
@@ -30,12 +35,7 @@ const route = useRoute()
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const problems = ref<ProblemFiltered[]>([])
|
const problems = ref<ProblemFiltered[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const tags = ref<{ id: number; name: string }[]>([])
|
const tags = ref<Tag[]>([])
|
||||||
|
|
||||||
const tagOptions = computed(() => [
|
|
||||||
{ label: "全部", value: "" },
|
|
||||||
...(tags.value?.map((t) => ({ label: t.name, value: t.name })) || []),
|
|
||||||
])
|
|
||||||
|
|
||||||
const query = reactive<Query>({
|
const query = reactive<Query>({
|
||||||
keyword: <string>route.query.keyword ?? "",
|
keyword: <string>route.query.keyword ?? "",
|
||||||
@@ -65,7 +65,10 @@ async function listProblems() {
|
|||||||
|
|
||||||
async function listTags() {
|
async function listTags() {
|
||||||
const res = await getProblemTagList()
|
const res = await getProblemTagList()
|
||||||
tags.value = res.data
|
tags.value = res.data.map((r: Omit<Tag, "checked">) => ({
|
||||||
|
...r,
|
||||||
|
checked: false,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function routerPush() {
|
function routerPush() {
|
||||||
@@ -79,6 +82,18 @@ function search(value: string) {
|
|||||||
query.keyword = value
|
query.keyword = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chooseTag(tag: Tag) {
|
||||||
|
query.tag = tag.checked ? "" : tag.name
|
||||||
|
tags.value = tags.value.map((t) => {
|
||||||
|
if (t.id === tag.id) {
|
||||||
|
t.checked = !t.checked
|
||||||
|
} else {
|
||||||
|
t.checked = false
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
query.keyword = ""
|
query.keyword = ""
|
||||||
query.tag = ""
|
query.tag = ""
|
||||||
@@ -161,14 +176,7 @@ function rowProps(row: ProblemFiltered) {
|
|||||||
:options="difficultyOptions"
|
:options="difficultyOptions"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="标签">
|
<n-form-item>
|
||||||
<n-select
|
|
||||||
class="select"
|
|
||||||
v-model:value="query.tag"
|
|
||||||
:options="tagOptions"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="搜索">
|
|
||||||
<n-input placeholder="输入编号或标题后回车" clearable @change="search" />
|
<n-input placeholder="输入编号或标题后回车" clearable @change="search" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
@@ -179,7 +187,21 @@ function rowProps(row: ProblemFiltered) {
|
|||||||
</n-space>
|
</n-space>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
<n-space>
|
||||||
|
<div class="tagTitle">标签</div>
|
||||||
|
<n-button
|
||||||
|
@click="chooseTag(tag)"
|
||||||
|
v-for="tag in tags"
|
||||||
|
:key="tag.id"
|
||||||
|
size="small"
|
||||||
|
secondary
|
||||||
|
:type="tag.checked ? 'success' : 'default'"
|
||||||
|
>
|
||||||
|
{{ tag.name }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
|
class="table"
|
||||||
striped
|
striped
|
||||||
size="small"
|
size="small"
|
||||||
:data="problems"
|
:data="problems"
|
||||||
@@ -194,7 +216,15 @@ function rowProps(row: ProblemFiltered) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.tagTitle {
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -106,76 +106,76 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
name: "home",
|
name: "admin home",
|
||||||
component: () => import("~/admin/setting/home.vue"),
|
component: () => import("~/admin/setting/home.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "config",
|
path: "config",
|
||||||
name: "config",
|
name: "admin config",
|
||||||
component: () => import("admin/setting/config.vue"),
|
component: () => import("admin/setting/config.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "announcement",
|
path: "announcement",
|
||||||
name: "announcement",
|
name: "admin announcement",
|
||||||
component: () => import("admin/setting/announcement.vue"),
|
component: () => import("admin/setting/announcement.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "user/list",
|
path: "user/list",
|
||||||
name: "user list",
|
name: "admin user list",
|
||||||
component: () => import("admin/user/list.vue"),
|
component: () => import("admin/user/list.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "user/importing",
|
path: "user/importing",
|
||||||
name: "user importing",
|
name: "admin user importing",
|
||||||
component: () => import("admin/user/import.vue"),
|
component: () => import("admin/user/import.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "problem/list",
|
path: "problem/list",
|
||||||
name: "problem list",
|
name: "admin problem list",
|
||||||
component: () => import("admin/problem/list.vue"),
|
component: () => import("admin/problem/list.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "problem/create",
|
path: "problem/create",
|
||||||
name: "problem create",
|
name: "admin problem create",
|
||||||
component: () => import("admin/problem/detail.vue"),
|
component: () => import("admin/problem/detail.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "problem/edit/:problemID",
|
path: "problem/edit/:problemID",
|
||||||
name: "problem edit",
|
name: "admin problem edit",
|
||||||
component: () => import("admin/problem/detail.vue"),
|
component: () => import("admin/problem/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/list",
|
path: "contest/list",
|
||||||
name: "contest list",
|
name: "admin contest list",
|
||||||
component: () => import("admin/contest/list.vue"),
|
component: () => import("admin/contest/list.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/create",
|
path: "contest/create",
|
||||||
name: "contest create",
|
name: "admin contest create",
|
||||||
component: () => import("admin/contest/detail.vue"),
|
component: () => import("admin/contest/detail.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/edit/:contestID",
|
path: "contest/edit/:contestID",
|
||||||
name: "contest edit",
|
name: "admin contest edit",
|
||||||
component: () => import("admin/contest/detail.vue"),
|
component: () => import("admin/contest/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/:contestID/problems",
|
path: "contest/:contestID/problems",
|
||||||
name: "contest problems",
|
name: "admin contest problems",
|
||||||
component: () => import("admin/contest/detail.vue"),
|
component: () => import("admin/contest/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/:contestID/problem/create",
|
path: "contest/:contestID/problem/create",
|
||||||
name: "contest problem create",
|
name: "admin contest problem create",
|
||||||
component: () => import("admin/problem/detail.vue"),
|
component: () => import("admin/problem/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/:contestID/problem/edit/:problemID",
|
path: "contest/:contestID/problem/edit/:problemID",
|
||||||
name: "contest problem edit",
|
name: "admin contest problem edit",
|
||||||
component: () => import("admin/problem/detail.vue"),
|
component: () => import("admin/problem/detail.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const options: MenuOption[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(RouterLink, { to: "/admin" }, { default: () => "首页" }),
|
label: () => h(RouterLink, { to: "/admin" }, { default: () => "首页" }),
|
||||||
key: "home",
|
key: "admin home",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "题目",
|
label: "题目",
|
||||||
@@ -24,7 +24,7 @@ const options: MenuOption[] = [
|
|||||||
{ to: "/admin/problem/list" },
|
{ to: "/admin/problem/list" },
|
||||||
{ default: () => "题目列表" }
|
{ default: () => "题目列表" }
|
||||||
),
|
),
|
||||||
key: "problem list",
|
key: "admin problem list",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
@@ -33,13 +33,13 @@ const options: MenuOption[] = [
|
|||||||
{ to: "/admin/problem/create" },
|
{ to: "/admin/problem/create" },
|
||||||
{ default: () => "创建题目" }
|
{ default: () => "创建题目" }
|
||||||
),
|
),
|
||||||
key: "problem create",
|
key: "admin problem create",
|
||||||
},
|
},
|
||||||
{ label: "用户", key: "user", disabled: true },
|
{ label: "用户", key: "user", disabled: true },
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户列表" }),
|
h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户列表" }),
|
||||||
key: "user list",
|
key: "admin user list",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
@@ -48,7 +48,7 @@ const options: MenuOption[] = [
|
|||||||
{ to: "/admin/user/importing" },
|
{ to: "/admin/user/importing" },
|
||||||
{ default: () => "导入用户" }
|
{ default: () => "导入用户" }
|
||||||
),
|
),
|
||||||
key: "user importing",
|
key: "admin user importing",
|
||||||
},
|
},
|
||||||
{ label: "比赛", key: "contest", disabled: true },
|
{ label: "比赛", key: "contest", disabled: true },
|
||||||
{
|
{
|
||||||
@@ -58,7 +58,7 @@ const options: MenuOption[] = [
|
|||||||
{ to: "/admin/contest/list" },
|
{ to: "/admin/contest/list" },
|
||||||
{ default: () => "比赛列表" }
|
{ default: () => "比赛列表" }
|
||||||
),
|
),
|
||||||
key: "contest list",
|
key: "admin contest list",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
@@ -67,13 +67,13 @@ const options: MenuOption[] = [
|
|||||||
{ to: "/admin/contest/create" },
|
{ to: "/admin/contest/create" },
|
||||||
{ default: () => "创建比赛" }
|
{ default: () => "创建比赛" }
|
||||||
),
|
),
|
||||||
key: "contest create",
|
key: "admin contest create",
|
||||||
},
|
},
|
||||||
{ label: "其他", key: "other", disabled: true },
|
{ label: "其他", key: "other", disabled: true },
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
h(RouterLink, { to: "/admin/config" }, { default: () => "系统配置" }),
|
h(RouterLink, { to: "/admin/config" }, { default: () => "系统配置" }),
|
||||||
key: "config",
|
key: "admin config",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
@@ -82,7 +82,7 @@ const options: MenuOption[] = [
|
|||||||
{ to: "/admin/announcement" },
|
{ to: "/admin/announcement" },
|
||||||
{ default: () => "公告配置" }
|
{ default: () => "公告配置" }
|
||||||
),
|
),
|
||||||
key: "announcement",
|
key: "admin announcement",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ const active = computed(() => (route.name as string) || "home")
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-layout has-sider position="absolute">
|
<n-layout has-sider position="absolute">
|
||||||
<n-layout-sider bordered :native-scrollbar="false">
|
<n-layout-sider width="160" bordered :native-scrollbar="false">
|
||||||
<n-menu :options="options" :value="active" />
|
<n-menu :options="options" :value="active" />
|
||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
<n-layout-content content-style="padding: 16px">
|
<n-layout-content content-style="padding: 16px">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getTime, intervalToDuration, parseISO } from "date-fns"
|
import { getTime, intervalToDuration, parseISO } from "date-fns"
|
||||||
import { STORAGE_KEY } from "./constants"
|
import { STORAGE_KEY } from "./constants"
|
||||||
|
import { User } from "./types"
|
||||||
|
|
||||||
export function getACRate(acCount: number, totalCount: number) {
|
export function getACRate(acCount: number, totalCount: number) {
|
||||||
let rate = totalCount === 0 ? 0.0 : ((acCount / totalCount) * 100).toFixed(2)
|
let rate = totalCount === 0 ? 0.0 : ((acCount / totalCount) * 100).toFixed(2)
|
||||||
@@ -103,3 +104,28 @@ export function debounce(fn: Function, n = 100) {
|
|||||||
}, n)
|
}, n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export interface User {
|
|||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
admin_type: string
|
admin_type: "Regular User" | "Super Admin" | "Admin"
|
||||||
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