fix contest conflict.

This commit is contained in:
2023-03-16 14:49:34 +08:00
parent 51328770c6
commit 0fa885d892
15 changed files with 290 additions and 148 deletions

View File

@@ -1,22 +1,10 @@
import http from "utils/http"
import { Problem } from "~/utils/types"
export async function getProblemList(
offset = 0,
limit = 10,
searchParams: any = {}
) {
let params: any = {
paging: true,
offset,
limit,
}
Object.keys(searchParams).forEach((element) => {
if (searchParams[element]) {
params[element] = searchParams[element]
}
export async function getProblemList(offset = 0, limit = 10, keyword: string) {
const res = await http.get("admin/problem", {
params: { paging: true, offset, limit, keyword },
})
const res = await http.get("admin/problem", { params })
return {
results: res.data.results.map((result: Problem) => ({
id: result.id,
@@ -41,3 +29,9 @@ export function editProblem(problem: Problem) {
export function getProblem(id: number) {
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 },
})
}

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

View File

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

View File

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

View File

@@ -1,13 +1,10 @@
<script setup lang="ts">
import { getProblemList, getProblem, editProblem } from "../api"
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 { parseTime } from "~/utils/functions"
import DownloadTestcases from "./components/DownloadTestcases.vue"
import DeleteProblem from "./components/DeleteProblem.vue"
const router = useRouter()
import Actions from "./components/Actions.vue"
const total = ref(0)
const problems = ref<AdminProblemFiltered[]>([])
@@ -41,38 +38,14 @@ const columns: DataTableColumn<AdminProblemFiltered>[] = [
},
{
key: "edit",
render: (row) =>
h(
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 }),
width: 200,
render: (row) => h(Actions, { problemID: row.id, onDeleted: listProblems }),
},
]
async function listProblems() {
const offset = (query.page - 1) * query.limit
const res = await getProblemList(offset, query.limit, {
keyword: query.keyword,
})
const res = await getProblemList(offset, query.limit, query.keyword)
total.value = res.total
problems.value = res.results
}

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

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

View File

@@ -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>
<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>
<style scoped></style>

34
src/components.d.ts vendored
View File

@@ -9,31 +9,31 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
IEpBell: typeof import("~icons/ep/bell")["default"]
IEpCaretRight: typeof import("~icons/ep/caret-right")["default"]
IEpLoading: typeof import("~icons/ep/loading")["default"]
IEpLock: typeof import("~icons/ep/lock")["default"]
IEpBell: typeof import('~icons/ep/bell')['default']
IEpCaretRight: typeof import('~icons/ep/caret-right')['default']
IEpLoading: typeof import('~icons/ep/loading')['default']
IEpLock: typeof import('~icons/ep/lock')['default']
IEpMenu: typeof import('~icons/ep/menu')['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']
NAlert: typeof import('naive-ui')['NAlert']
NAvatar: typeof import("naive-ui")["NAvatar"]
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import("naive-ui")["NCard"]
NCard: typeof import('naive-ui')['NCard']
NCode: typeof import("naive-ui")["NCode"]
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import("naive-ui")["NDescriptions"]
NDescriptionsItem: typeof import("naive-ui")["NDescriptionsItem"]
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDropdown: typeof import('naive-ui')['NDropdown']
NEmpty: typeof import("naive-ui")["NEmpty"]
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import("naive-ui")["NGi"]
NGrid: typeof import("naive-ui")["NGrid"]
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NLayout: typeof import('naive-ui')['NLayout']
@@ -45,14 +45,14 @@ declare module '@vue/runtime-core' {
NModal: typeof import('naive-ui')['NModal']
NPagination: typeof import('naive-ui')['NPagination']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import("naive-ui")["NPopover"]
NScrollbar: typeof import("naive-ui")["NScrollbar"]
NPopover: typeof import('naive-ui')['NPopover']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import("naive-ui")["NSwitch"]
NTabPane: typeof import("naive-ui")["NTabPane"]
NTabs: typeof import("naive-ui")["NTabs"]
NTag: typeof import("naive-ui")["NTag"]
NSwitch: typeof import('naive-ui')['NSwitch']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NTooltip: typeof import('naive-ui')['NTooltip']
NUpload: typeof import("naive-ui")["NUpload"]
RouterLink: typeof import('vue-router')['RouterLink']

View File

@@ -94,7 +94,9 @@ function select(key: string) {
<n-space>
<n-button @click="reset">重置</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-form-item>
</n-form>

View File

@@ -1,14 +1,19 @@
<script setup lang="ts">
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 { isDesktop } from "~/shared/composables/breakpoints"
import { getProblemList, getProblemTagList, getRandomProblemID } from "oj/api"
import Pagination from "~/shared/Pagination.vue"
import { DataTableColumn, NSpace, NTag } from "naive-ui"
import ProblemStatus from "./components/ProblemStatus.vue"
interface Tag {
id: number
name: string
checked: boolean
}
interface Query {
keyword: string
difficulty: string
@@ -30,12 +35,7 @@ const route = useRoute()
const userStore = useUserStore()
const problems = ref<ProblemFiltered[]>([])
const total = ref(0)
const tags = ref<{ id: number; name: string }[]>([])
const tagOptions = computed(() => [
{ label: "全部", value: "" },
...(tags.value?.map((t) => ({ label: t.name, value: t.name })) || []),
])
const tags = ref<Tag[]>([])
const query = reactive<Query>({
keyword: <string>route.query.keyword ?? "",
@@ -65,7 +65,10 @@ async function listProblems() {
async function listTags() {
const res = await getProblemTagList()
tags.value = res.data
tags.value = res.data.map((r: Omit<Tag, "checked">) => ({
...r,
checked: false,
}))
}
function routerPush() {
@@ -79,6 +82,18 @@ function search(value: string) {
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() {
query.keyword = ""
query.tag = ""
@@ -161,14 +176,7 @@ function rowProps(row: ProblemFiltered) {
:options="difficultyOptions"
/>
</n-form-item>
<n-form-item label="标签">
<n-select
class="select"
v-model:value="query.tag"
:options="tagOptions"
/>
</n-form-item>
<n-form-item label="搜索">
<n-form-item>
<n-input placeholder="输入编号或标题后回车" clearable @change="search" />
</n-form-item>
<n-form-item>
@@ -179,7 +187,21 @@ function rowProps(row: ProblemFiltered) {
</n-space>
</n-form-item>
</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
class="table"
striped
size="small"
:data="problems"
@@ -194,7 +216,15 @@ function rowProps(row: ProblemFiltered) {
</template>
<style scoped>
.tagTitle {
line-height: 28px;
}
.select {
width: 120px;
}
.table {
margin-top: 24px;
}
</style>

View File

@@ -106,76 +106,76 @@ export const routes: RouteRecordRaw[] = [
children: [
{
path: "",
name: "home",
name: "admin home",
component: () => import("~/admin/setting/home.vue"),
},
{
path: "config",
name: "config",
name: "admin config",
component: () => import("admin/setting/config.vue"),
},
{
path: "announcement",
name: "announcement",
name: "admin announcement",
component: () => import("admin/setting/announcement.vue"),
},
{
path: "user/list",
name: "user list",
name: "admin user list",
component: () => import("admin/user/list.vue"),
},
{
path: "user/importing",
name: "user importing",
name: "admin user importing",
component: () => import("admin/user/import.vue"),
},
{
path: "problem/list",
name: "problem list",
name: "admin problem list",
component: () => import("admin/problem/list.vue"),
},
{
path: "problem/create",
name: "problem create",
name: "admin problem create",
component: () => import("admin/problem/detail.vue"),
},
{
path: "problem/edit/:problemID",
name: "problem edit",
name: "admin problem edit",
component: () => import("admin/problem/detail.vue"),
props: true,
},
{
path: "contest/list",
name: "contest list",
name: "admin contest list",
component: () => import("admin/contest/list.vue"),
},
{
path: "contest/create",
name: "contest create",
name: "admin contest create",
component: () => import("admin/contest/detail.vue"),
},
{
path: "contest/edit/:contestID",
name: "contest edit",
name: "admin contest edit",
component: () => import("admin/contest/detail.vue"),
props: true,
},
{
path: "contest/:contestID/problems",
name: "contest problems",
name: "admin contest problems",
component: () => import("admin/contest/detail.vue"),
props: true,
},
{
path: "contest/:contestID/problem/create",
name: "contest problem create",
name: "admin contest problem create",
component: () => import("admin/problem/detail.vue"),
props: true,
},
{
path: "contest/:contestID/problem/edit/:problemID",
name: "contest problem edit",
name: "admin contest problem edit",
component: () => import("admin/problem/detail.vue"),
props: true,
},

View File

@@ -10,7 +10,7 @@ const options: MenuOption[] = [
},
{
label: () => h(RouterLink, { to: "/admin" }, { default: () => "首页" }),
key: "home",
key: "admin home",
},
{
label: "题目",
@@ -24,7 +24,7 @@ const options: MenuOption[] = [
{ to: "/admin/problem/list" },
{ default: () => "题目列表" }
),
key: "problem list",
key: "admin problem list",
},
{
label: () =>
@@ -33,13 +33,13 @@ const options: MenuOption[] = [
{ to: "/admin/problem/create" },
{ default: () => "创建题目" }
),
key: "problem create",
key: "admin problem create",
},
{ label: "用户", key: "user", disabled: true },
{
label: () =>
h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户列表" }),
key: "user list",
key: "admin user list",
},
{
label: () =>
@@ -48,7 +48,7 @@ const options: MenuOption[] = [
{ to: "/admin/user/importing" },
{ default: () => "导入用户" }
),
key: "user importing",
key: "admin user importing",
},
{ label: "比赛", key: "contest", disabled: true },
{
@@ -58,7 +58,7 @@ const options: MenuOption[] = [
{ to: "/admin/contest/list" },
{ default: () => "比赛列表" }
),
key: "contest list",
key: "admin contest list",
},
{
label: () =>
@@ -67,13 +67,13 @@ const options: MenuOption[] = [
{ to: "/admin/contest/create" },
{ default: () => "创建比赛" }
),
key: "contest create",
key: "admin contest create",
},
{ label: "其他", key: "other", disabled: true },
{
label: () =>
h(RouterLink, { to: "/admin/config" }, { default: () => "系统配置" }),
key: "config",
key: "admin config",
},
{
label: () =>
@@ -82,7 +82,7 @@ const options: MenuOption[] = [
{ to: "/admin/announcement" },
{ default: () => "公告配置" }
),
key: "announcement",
key: "admin announcement",
},
]
@@ -91,7 +91,7 @@ const active = computed(() => (route.name as string) || "home")
<template>
<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-layout-sider>
<n-layout-content content-style="padding: 16px">

View File

@@ -1,5 +1,6 @@
import { getTime, intervalToDuration, parseISO } from "date-fns"
import { STORAGE_KEY } from "./constants"
import { User } from "./types"
export function getACRate(acCount: number, totalCount: number) {
let rate = totalCount === 0 ? 0.0 : ((acCount / totalCount) * 100).toFixed(2)
@@ -103,3 +104,28 @@ export function debounce(fn: Function, n = 100) {
}, 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
}

View File

@@ -37,7 +37,7 @@ export interface User {
id: number
username: string
email: string
admin_type: string
admin_type: "Regular User" | "Super Admin" | "Admin"
problem_permission: string
create_time: Date
last_login: Date