add contest problem.

This commit is contained in:
2023-01-31 15:38:29 +08:00
parent 6aa722c64a
commit d5d6491d6d
16 changed files with 210 additions and 89 deletions

View File

@@ -27,9 +27,9 @@ hljs.registerLanguage("go", go)
:date-locale="dateZhCN"
:hljs="hljs"
>
<n-notification-provider>
<n-message-provider>
<router-view></router-view>
</n-notification-provider>
</n-message-provider>
</n-config-provider>
</template>

1
src/components.d.ts vendored
View File

@@ -32,6 +32,7 @@ declare module '@vue/runtime-core' {
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPagination: typeof import('naive-ui')['NPagination']

View File

@@ -117,6 +117,9 @@ export function checkContestPassword(contestID: string, password: string) {
})
}
export function getContestProblem(contestID: string) {
return http.get("contest/problem", { params: { contest_id: contestID } })
export async function getContestProblem(contestID: string) {
const res = await http.get("contest/problem", {
params: { contest_id: contestID },
})
return res.data.map(filterResult)
}

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { Contest, Problem } from "utils/types"
import { CONTEST_STATUS, ContestStatus, ContestType } from "utils/constants"
import { getACRate, parseTime } from "utils/functions"
import { parseTime } from "utils/functions"
import {
getContest,
getContestAccess,
@@ -10,12 +10,13 @@ import {
} from "../api"
import { isDesktop } from "~/shared/composables/breakpoints"
import ContestTypeVue from "./components/ContestType.vue"
import { DataTableColumn } from "naive-ui"
import { useUserStore } from "~/shared/store/user"
const props = defineProps<{
contestID: string
}>()
const message = useMessage()
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const contest = ref<Contest>()
@@ -33,10 +34,11 @@ async function init() {
getProblems()
}
onMounted(init)
async function getProblems() {
try {
const res = await getContestProblem(props.contestID)
problems.value = res.data
problems.value = await getContestProblem(props.contestID)
} catch (err) {
toggleAccsess(false)
}
@@ -51,9 +53,10 @@ async function checkPassword() {
}
} catch (err) {
toggleAccsess(false)
message.error("密码错误")
}
}
onMounted(init)
const isContestAdmin = computed(
() =>
userStore.isSuperAdmin ||
@@ -64,28 +67,23 @@ const passwordFormVisible = computed(
() =>
contest.value?.contest_type === ContestType.private &&
!access.value &&
!isContestAdmin
!isContestAdmin.value
)
const problemsColumns: DataTableColumn<Problem>[] = [
{ title: "编号", key: "_id", width: 100 },
{ title: "标题", key: "title" },
{ title: "总提交数", key: "submission_number", width: 100 },
{
title: "通过率",
key: "rate",
width: 100,
render: (row) => getACRate(row.accepted_number, row.submission_number),
},
]
function rowProps(row: Problem) {
return {
style: "cursor: pointer",
onClick() {
router.push(`/contest/${props.contestID}/problem/${row._id}`)
},
const contestMenuVisible = computed(() => {
if (isContestAdmin) return true
if (contest.value?.contest_type === ContestType.public) {
}
return access.value
})
function goto(name: string) {
router.push({ name: "contest " + name })
}
function getCurrentType(name: string): "primary" | "default" {
if (route.name === "contest " + name) return "primary"
return "default"
}
</script>
@@ -104,27 +102,42 @@ function rowProps(row: Problem) {
</n-icon>
</n-space>
<div v-html="contest.description"></div>
<n-form inline label-placement="left">
<n-form :inline="isDesktop" label-placement="left">
<n-form-item v-if="passwordFormVisible" label="需要输入密码才能看到题目">
<n-input
name="ContestPassword"
type="password"
v-model:value="password"
@change="checkPassword"
/>
</n-form-item>
<n-form-item v-if="passwordFormVisible">
<n-button @click="checkPassword" :disabled="!password">确认</n-button>
</n-form-item>
<n-form-item>
<n-form-item v-if="contestMenuVisible">
<n-space>
<n-button type="primary">比赛题目</n-button>
<n-button>提交信息</n-button>
<n-button>比赛排名</n-button>
<n-button>管理员助手</n-button>
<n-button
:type="getCurrentType('problems')"
@click="goto('problems')"
>
比赛题目
</n-button>
<n-button
:type="getCurrentType('submissions')"
@click="goto('submissions')"
>
提交信息
</n-button>
<n-button :type="getCurrentType('rank')" @click="goto('rank')">
比赛排名
</n-button>
<n-button :type="getCurrentType('helper')" @click="goto('helper')">
管理员助手
</n-button>
</n-space>
</n-form-item>
</n-form>
<n-descriptions bordered :column="isDesktop ? 5 : 2">
<n-descriptions class="margin" bordered :column="isDesktop ? 5 : 2">
<n-descriptions-item
:span="isDesktop ? 1 : 2"
v-if="contest.status !== ContestStatus.finished"
@@ -156,16 +169,8 @@ function rowProps(row: Problem) {
{{ contest.created_by.username }}
</n-descriptions-item>
</n-descriptions>
<router-view :problems="problems"></router-view>
</div>
<n-data-table
striped
size="small"
class="problems"
:data="problems"
:columns="problemsColumns"
:row-props="rowProps"
v-if="problems?.length"
></n-data-table>
</template>
<style scoped>
@@ -177,7 +182,7 @@ function rowProps(row: Problem) {
transform: translateY(2px);
}
.problems {
margin-top: 24px;
.margin {
margin-bottom: 24px;
}
</style>

View File

@@ -30,6 +30,17 @@ const options: SelectOption[] = [
]
const columns: DataTableColumn<Contest>[] = [
{
title: "状态",
key: "status",
width: 100,
render: (row) =>
h(
NTag,
{ type: CONTEST_STATUS[row.status]["type"] },
() => CONTEST_STATUS[row.status]["name"]
),
},
{
title: "比赛",
key: "title",
@@ -48,17 +59,6 @@ const columns: DataTableColumn<Contest>[] = [
width: 180,
render: (row) => duration(row.start_time, row.end_time),
},
{
title: "状态",
key: "status",
width: 100,
render: (row) =>
h(
NTag,
{ type: CONTEST_STATUS[row.status]["type"] },
() => CONTEST_STATUS[row.status]["name"]
),
},
]
async function listContests() {
@@ -129,7 +129,7 @@ function rowProps(row: Contest) {
</n-form-item>
<n-form-item label="搜索比赛标题">
<n-input
placeholder="输入后回车"
placeholder="输入后回车或点击搜索"
v-model:value="query.keyword"
@change="search"
/>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template></template>
<style scoped></style>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { DataTableColumn } from "naive-ui"
import { ProblemFiltered } from "utils/types"
import ProblemStatus from "~/oj/problem/components/ProblemStatus.vue"
const props = defineProps<{
contestID: string
problems: ProblemFiltered[]
}>()
const router = useRouter()
const problemsColumns: DataTableColumn<ProblemFiltered>[] = [
{
title: "状态",
key: "status",
width: 60,
render: (row) => h(ProblemStatus, { status: row.status }),
},
{ title: "编号", key: "_id", width: 60 },
{ title: "题目", key: "title", minWidth: 200 },
{ title: "总提交数", key: "submission", width: 100 },
{ title: "通过率", key: "rate", width: 100 },
]
function rowProps(row: ProblemFiltered) {
return {
style: "cursor: pointer",
onClick() {
router.push(`/contest/${props.contestID}/problem/${row._id}`)
},
}
}
</script>
<template>
<n-data-table
striped
size="small"
class="problems"
:data="problems"
:columns="problemsColumns"
:row-props="rowProps"
v-if="problems?.length"
/>
</template>
<style scoped></style>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template></template>
<style scoped></style>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template></template>
<style scoped></style>

View File

@@ -14,11 +14,10 @@ const data = computed(() => {
const status = props.problem.statistic_info
const labels = []
for (let i in status) {
if (status[i] === 0) {
delete status[i]
if (status[i] !== 0) {
// @ts-ignore
labels.push(JUDGE_STATUS[i]["name"])
}
// @ts-ignore
labels.push(JUDGE_STATUS[i]["name"])
}
return {
labels,

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import { Select, SemiSelect } from "@element-plus/icons-vue"
import { useThemeVars } from "naive-ui"
const theme = useThemeVars()
const props = defineProps<{
status: "not_test" | "passed" | "failed"
}>()
const showIcon = computed(() => props.status !== "not_test")
const color = computed(() => {
if (props.status === "passed") return theme.value.successColor
if (props.status === "failed") return theme.value.errorColor
})
const Icon = computed(() => {
if (props.status === "passed") return Select
if (props.status === "failed") return SemiSelect
})
</script>
<template>
<n-icon v-if="showIcon" :color="color">
<component :is="Icon"></component>
</n-icon>
</template>
<style scoped></style>

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { useUserStore } from "~/shared/store/user"
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, NIcon, NSpace, NTag, useThemeVars } from "naive-ui"
import { Select, SemiSelect } from "@element-plus/icons-vue"
import { DataTableColumn, NSpace, NTag } from "naive-ui"
import ProblemStatus from "./components/ProblemStatus.vue"
interface Query {
keyword: string
@@ -16,16 +17,6 @@ interface Query {
limit: number
}
interface ProblemFiltered {
_id: string
title: string
difficulty: "简单" | "中等" | "困难"
tags: string[]
submission: number
rate: string
status: "not_test" | "passed" | "failed"
}
const difficultyOptions = [
{ label: "全部", value: "" },
{ label: "简单", value: "Low" },
@@ -35,7 +26,7 @@ const difficultyOptions = [
const router = useRouter()
const route = useRoute()
const theme = useThemeVars()
const userStore = useUserStore()
const problems = ref<ProblemFiltered[]>([])
const total = ref(0)
@@ -129,18 +120,10 @@ const columns: DataTableColumn<ProblemFiltered>[] = [
title: "状态",
key: "status",
width: 60,
render: (row) => {
if (row.status === "passed") {
return h(NIcon, { color: theme.value.successColor }, () => h(Select))
} else if (row.status === "failed") {
return h(NIcon, { color: theme.value.errorColor }, () => h(SemiSelect))
} else {
return null
}
},
render: (row) => h(ProblemStatus, { status: row.status }),
},
{ title: "编号", key: "_id", width: 100 },
{ title: "题", key: "title", minWidth: 200 },
{ title: "题", key: "title", minWidth: 200 },
{
title: "难度",
key: "difficulty",

View File

@@ -63,7 +63,6 @@ onMounted(init)
</n-space>
<n-code
class="code"
word-wrap
:language="LANGUAGE_VALUE[submission.language]"
:code="submission.code"
show-line-numbers

View File

@@ -25,7 +25,7 @@ interface Query {
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const notification = useNotification()
const message = useMessage()
const submissions = ref([])
const total = ref(0)
@@ -90,7 +90,7 @@ function clear() {
async function rejudge(submissionID: string) {
await adminRejudge(submissionID)
notification.success({ title: "重新判分成功", duration: 2000 })
message.success("重新判分成功")
listSubmissions()
}
@@ -209,7 +209,7 @@ const columns = computed(() => {
<n-switch v-model:value="query.myself" />
</n-form-item>
<n-form-item label="搜索用户">
<n-input @change="search" clearable placeholder="输入后回车" />
<n-input @change="search" clearable placeholder="输入后回车或点击搜索" />
</n-form-item>
<n-form-item>
<n-space>

View File

@@ -34,6 +34,37 @@ export const routes: RouteRecordRaw[] = [
path: "contest/:contestID",
component: () => import("oj/contest/detail.vue"),
props: true,
meta: { requiresAuth: true },
children: [
{
path: "",
component: () => import("oj/contest/pages/problems.vue"),
props: true,
meta: { requiresAuth: true },
name: "contest problems",
},
{
path: "submissions",
component: () => import("oj/contest/pages/submissions.vue"),
props: true,
meta: { requiresAuth: true },
name: "contest submissions",
},
{
path: "rank",
component: () => import("oj/contest/pages/rank.vue"),
props: true,
meta: { requiresAuth: true },
name: "contest rank",
},
{
path: "helper",
component: () => import("~/oj/contest/pages/helper.vue"),
props: true,
meta: { requiresAuth: true },
name: "contest helper",
},
],
},
{
path: "contest/:contestID/problem/:problemID",

View File

@@ -102,6 +102,16 @@ export interface Problem {
my_status: number
}
export interface ProblemFiltered {
_id: string
title: string
difficulty: "简单" | "中等" | "困难"
tags: string[]
submission: number
rate: string
status: "not_test" | "passed" | "failed"
}
export interface Code {
language: LANGUAGE
value: string