add contest problem.
This commit is contained in:
@@ -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
1
src/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
5
src/oj/contest/pages/helper.vue
Normal file
5
src/oj/contest/pages/helper.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template></template>
|
||||
|
||||
<style scoped></style>
|
||||
47
src/oj/contest/pages/problems.vue
Normal file
47
src/oj/contest/pages/problems.vue
Normal 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>
|
||||
5
src/oj/contest/pages/rank.vue
Normal file
5
src/oj/contest/pages/rank.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template></template>
|
||||
|
||||
<style scoped></style>
|
||||
5
src/oj/contest/pages/submissions.vue
Normal file
5
src/oj/contest/pages/submissions.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template></template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -14,12 +14,11 @@ 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"])
|
||||
}
|
||||
}
|
||||
return {
|
||||
labels,
|
||||
datasets: [{ data: Object.values(status), hoverOffset: 4 }],
|
||||
|
||||
28
src/oj/problem/components/ProblemStatus.vue
Normal file
28
src/oj/problem/components/ProblemStatus.vue
Normal 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>
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user