contest list.
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@vueuse/integrations": "^9.11.0",
|
"@vueuse/integrations": "^9.11.0",
|
||||||
"axios": "1.2.2",
|
"axios": "1.2.2",
|
||||||
"copy-text-to-clipboard": "^3.0.1",
|
"copy-text-to-clipboard": "^3.0.1",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
"naive-ui": "^2.34.3",
|
"naive-ui": "^2.34.3",
|
||||||
"party-js": "^2.2.0",
|
"party-js": "^2.2.0",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@vueuse/integrations": "^9.11.0",
|
"@vueuse/integrations": "^9.11.0",
|
||||||
"axios": "1.2.2",
|
"axios": "1.2.2",
|
||||||
"copy-text-to-clipboard": "^3.0.1",
|
"copy-text-to-clipboard": "^3.0.1",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
"naive-ui": "^2.34.3",
|
"naive-ui": "^2.34.3",
|
||||||
"party-js": "^2.2.0",
|
"party-js": "^2.2.0",
|
||||||
|
|||||||
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@@ -10,6 +10,7 @@ declare module '@vue/runtime-core' {
|
|||||||
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']
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
|
|||||||
@@ -92,3 +92,18 @@ export function adminRejudge(id: string) {
|
|||||||
params: { id },
|
params: { id },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRank(offset: number, limit: number) {
|
||||||
|
return http.get("user_rank", {
|
||||||
|
params: { offset, limit, rule: "acm" },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContestList(query: {
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
keyword: string
|
||||||
|
status: string
|
||||||
|
}) {
|
||||||
|
return http.get("contests", { params: query })
|
||||||
|
}
|
||||||
|
|||||||
21
src/oj/contest/components/ContestTitle.vue
Normal file
21
src/oj/contest/components/ContestTitle.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Contest } from "~/utils/types"
|
||||||
|
|
||||||
|
defineProps<{ contest: Contest }>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<n-space>
|
||||||
|
<span>{{ contest.title }}</span>
|
||||||
|
<n-icon
|
||||||
|
class="lockIcon"
|
||||||
|
v-if="contest.contest_type === 'Password Protected'"
|
||||||
|
>
|
||||||
|
<i-ep-lock />
|
||||||
|
</n-icon>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.lockIcon {
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,161 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { DataTableColumn, NTag, SelectOption } from "naive-ui"
|
||||||
|
import { getContestList } from "oj/api"
|
||||||
|
import Pagination from "~/shared/Pagination.vue"
|
||||||
|
import { filterEmptyValue, parseTime, duration } from "utils/functions"
|
||||||
|
import { Contest } from "utils/types"
|
||||||
|
import { CONTEST_STATUS } from "~/utils/constants"
|
||||||
|
import ContestTitle from "./components/ContestTitle.vue"
|
||||||
|
import { useUserStore } from "~/shared/store/user"
|
||||||
|
import { toggleLogin } from "~/shared/composables/modal"
|
||||||
|
|
||||||
<template>contest list</template>
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const query = reactive({
|
||||||
|
page: parseInt(<string>route.query.page) || 1,
|
||||||
|
limit: parseInt(<string>route.query.limit) || 10,
|
||||||
|
keyword: <string>route.query.keyword ?? "",
|
||||||
|
status: <string>route.query.status ?? "",
|
||||||
|
})
|
||||||
|
const data = ref<Contest[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
<style scoped></style>
|
const options: SelectOption[] = [
|
||||||
|
{ label: "全部", value: "" },
|
||||||
|
{ label: "未开始", value: "1" },
|
||||||
|
{ label: "进行中", value: "0" },
|
||||||
|
{ label: "已结束", value: "-1" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const columns: DataTableColumn<Contest>[] = [
|
||||||
|
{
|
||||||
|
title: "比赛",
|
||||||
|
key: "title",
|
||||||
|
minWidth: 360,
|
||||||
|
render: (row) => h(ContestTitle, { contest: row }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "开始时间",
|
||||||
|
key: "start_time",
|
||||||
|
width: 180,
|
||||||
|
render: (row) => parseTime(row.start_time),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "比赛时长",
|
||||||
|
key: "duration",
|
||||||
|
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() {
|
||||||
|
const offset = (query.page - 1) * query.limit
|
||||||
|
const res = await getContestList({
|
||||||
|
offset,
|
||||||
|
limit: query.limit,
|
||||||
|
keyword: query.keyword,
|
||||||
|
status: query.status,
|
||||||
|
})
|
||||||
|
data.value = res.data.results
|
||||||
|
total.value = res.data.total
|
||||||
|
}
|
||||||
|
|
||||||
|
function routerPush() {
|
||||||
|
router.push({
|
||||||
|
path: route.path,
|
||||||
|
query: filterEmptyValue(query),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(value: string) {
|
||||||
|
query.keyword = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
query.keyword = ""
|
||||||
|
query.status = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(listContests)
|
||||||
|
watch(() => query.page, routerPush)
|
||||||
|
watch(
|
||||||
|
() => [query.limit, query.keyword, query.status],
|
||||||
|
() => {
|
||||||
|
query.page = 1
|
||||||
|
routerPush()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => route.path === "/contest" && route.query,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) listContests()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function rowProps(row: Contest) {
|
||||||
|
return {
|
||||||
|
style: "cursor: pointer",
|
||||||
|
onClick() {
|
||||||
|
if (!userStore.isAuthed && row.contest_type === "Password Protected") {
|
||||||
|
toggleLogin(true)
|
||||||
|
} else {
|
||||||
|
router.push("/contest/" + row.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<n-form label-placement="left" inline>
|
||||||
|
<n-form-item label="状态">
|
||||||
|
<n-select
|
||||||
|
class="select"
|
||||||
|
:options="options"
|
||||||
|
v-model:value="query.status"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-input
|
||||||
|
placeholder="关键字"
|
||||||
|
v-model:value="query.keyword"
|
||||||
|
@change="search"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="search(query.keyword)">搜索</n-button>
|
||||||
|
<n-button @click="clear">重置</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<n-data-table
|
||||||
|
size="small"
|
||||||
|
striped
|
||||||
|
:columns="columns"
|
||||||
|
:data="data"
|
||||||
|
:row-props="rowProps"
|
||||||
|
/>
|
||||||
|
<Pagination
|
||||||
|
v-model:limit="query.limit"
|
||||||
|
v-model:page="query.page"
|
||||||
|
:total="total"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.select {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { submissionMemoryFormat, submissionTimeFormat } from "utils/functions"
|
|||||||
import { Problem, Submission, SubmitCodePayload } from "utils/types"
|
import { Problem, Submission, SubmitCodePayload } from "utils/types"
|
||||||
import { getSubmission, submitCode } from "oj/api"
|
import { getSubmission, submitCode } from "oj/api"
|
||||||
import SubmissionResultTag from "../../../shared/SubmissionResultTag.vue"
|
import SubmissionResultTag from "../../../shared/SubmissionResultTag.vue"
|
||||||
import { DataTableColumn } from "naive-ui"
|
import type { DataTableColumn } from "naive-ui"
|
||||||
|
|
||||||
const problem = inject<Ref<Problem>>("problem")
|
const problem = inject<Ref<Problem>>("problem")
|
||||||
|
|
||||||
@@ -235,9 +235,9 @@ const tabProps = {
|
|||||||
<n-data-table
|
<n-data-table
|
||||||
v-if="infoTable.length"
|
v-if="infoTable.length"
|
||||||
size="small"
|
size="small"
|
||||||
|
striped
|
||||||
:data="infoTable"
|
:data="infoTable"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
striped
|
|
||||||
/>
|
/>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
@@ -1,11 +1,72 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DataTableColumn } from "naive-ui"
|
import { DataTableColumn, NButton, NButtonGroup } from "naive-ui"
|
||||||
|
import Pagination from "~/shared/Pagination.vue"
|
||||||
|
import { Rank } from "utils/types"
|
||||||
|
import { getRank } from "oj/api"
|
||||||
|
import { getACRate } from "utils/functions"
|
||||||
|
|
||||||
const columns: DataTableColumn[] = []
|
const data = ref<Rank[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const query = reactive({
|
||||||
|
limit: 10,
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function listRanks() {
|
||||||
|
const offset = (query.page - 1) * query.limit
|
||||||
|
const res = await getRank(offset, query.limit)
|
||||||
|
data.value = res.data.results
|
||||||
|
total.value = res.data.total
|
||||||
|
}
|
||||||
|
const columns: DataTableColumn<Rank>[] = [
|
||||||
|
{
|
||||||
|
title: "排名",
|
||||||
|
key: "index",
|
||||||
|
minWidth: 60,
|
||||||
|
render: (_, index) =>
|
||||||
|
h("span", {}, index + (query.page - 1) * query.limit + 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "用户",
|
||||||
|
key: "username",
|
||||||
|
minWidth: 100,
|
||||||
|
render: (row) =>
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{ text: true, type: "info", onClick: () => {} },
|
||||||
|
() => row.user.username
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: "骚话", key: "mood", minWidth: 200 },
|
||||||
|
{ title: "已解决", key: "accepted_number", minWidth: 80 },
|
||||||
|
{ title: "提交数", key: "submission_number", minWidth: 80 },
|
||||||
|
{
|
||||||
|
title: "正确率",
|
||||||
|
key: "rate",
|
||||||
|
minWidth: 80,
|
||||||
|
render: (row) => getACRate(row.accepted_number, row.submission_number),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
watch(() => query.page, listRanks)
|
||||||
|
watch(
|
||||||
|
() => query.limit,
|
||||||
|
() => {
|
||||||
|
query.page = 1
|
||||||
|
listRanks()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(listRanks)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-data-table :columns="columns" />
|
<n-data-table striped size="small" :data="data" :columns="columns" />
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="query.page"
|
||||||
|
v-model:limit="query.limit"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ onMounted(init)
|
|||||||
<n-space>
|
<n-space>
|
||||||
<span>提交时间:{{ parseTime(submission.create_time) }}</span>
|
<span>提交时间:{{ parseTime(submission.create_time) }}</span>
|
||||||
<span>语言:{{ submission.language }}</span>
|
<span>语言:{{ submission.language }}</span>
|
||||||
<span>提交人 {{ submission.username }}</span>
|
<span>用户 {{ submission.username }}</span>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-card embedded>
|
<n-card embedded>
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ const columns = computed(() => {
|
|||||||
},
|
},
|
||||||
{ title: "语言", key: "language", width: 100 },
|
{ title: "语言", key: "language", width: 100 },
|
||||||
{
|
{
|
||||||
title: "提交者",
|
title: "用户",
|
||||||
key: "username",
|
key: "username",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
@@ -206,7 +206,7 @@ const columns = computed(() => {
|
|||||||
<n-form-item label="只看自己">
|
<n-form-item label="只看自己">
|
||||||
<n-switch v-model:value="query.myself" />
|
<n-switch v-model:value="query.myself" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="搜索提交者">
|
<n-form-item label="搜索用户">
|
||||||
<n-input @change="search" clearable placeholder="输入后回车" />
|
<n-input @change="search" clearable placeholder="输入后回车" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: "submission",
|
path: "submission",
|
||||||
component: () => import("oj/submission/list.vue"),
|
component: () => import("oj/submission/list.vue"),
|
||||||
meta: { requiresAuth: true },
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "submission/:submissionID",
|
path: "submission/:submissionID",
|
||||||
@@ -23,7 +22,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: "contest",
|
path: "contest",
|
||||||
component: () => import("oj/contest/list.vue"),
|
component: () => import("oj/contest/list.vue"),
|
||||||
meta: { requiresAuth: true },
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "contest/:contestID",
|
path: "contest/:contestID",
|
||||||
|
|||||||
@@ -70,32 +70,26 @@ export const JUDGE_STATUS: {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CONTEST_STATUS = {
|
export const CONTEST_STATUS: {
|
||||||
NOT_START: "1",
|
[key in "1" | "-1" | "0"]: {
|
||||||
UNDERWAY: "0",
|
name: string
|
||||||
ENDED: "-1",
|
type: "error" | "success" | "warning"
|
||||||
}
|
}
|
||||||
|
} = {
|
||||||
export const CONTEST_STATUS_REVERSE = {
|
|
||||||
"1": {
|
"1": {
|
||||||
name: "Not Started",
|
name: "未开始",
|
||||||
color: "yellow",
|
type: "warning",
|
||||||
},
|
},
|
||||||
"0": {
|
"0": {
|
||||||
name: "Underway",
|
name: "进行中",
|
||||||
color: "green",
|
type: "success",
|
||||||
},
|
},
|
||||||
"-1": {
|
"-1": {
|
||||||
name: "Ended",
|
name: "已结束",
|
||||||
color: "red",
|
type: "error",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RULE_TYPE = {
|
|
||||||
ACM: "ACM",
|
|
||||||
OI: "OI",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CONTEST_TYPE = {
|
export const CONTEST_TYPE = {
|
||||||
PUBLIC: "Public",
|
PUBLIC: "Public",
|
||||||
PRIVATE: "Password Protected",
|
PRIVATE: "Password Protected",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { intervalToDuration } from "date-fns"
|
||||||
import { STORAGE_KEY } from "./constants"
|
import { STORAGE_KEY } from "./constants"
|
||||||
|
|
||||||
export function getACRate(acCount: number, totalCount: number) {
|
export function getACRate(acCount: number, totalCount: number) {
|
||||||
@@ -40,6 +41,30 @@ export function parseTime(utc: Date, format = "YYYY年M月D日") {
|
|||||||
return time.value
|
return time.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function duration(start: Date, end: Date): string {
|
||||||
|
const duration = intervalToDuration({
|
||||||
|
start: Date.parse(start.toString()),
|
||||||
|
end: Date.parse(end.toString()),
|
||||||
|
})
|
||||||
|
let result = ""
|
||||||
|
if (duration.years) {
|
||||||
|
result += duration.years + "年"
|
||||||
|
}
|
||||||
|
if (duration.months) {
|
||||||
|
result += duration.months + "月"
|
||||||
|
}
|
||||||
|
if (duration.days) {
|
||||||
|
result += duration.days + "天"
|
||||||
|
}
|
||||||
|
if (duration.hours) {
|
||||||
|
result += duration.hours + "小时"
|
||||||
|
}
|
||||||
|
if (duration.minutes) {
|
||||||
|
result += duration.minutes + "分钟"
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export function submissionMemoryFormat(memory: number | string | undefined) {
|
export function submissionMemoryFormat(memory: number | string | undefined) {
|
||||||
if (memory === undefined) return "--"
|
if (memory === undefined) return "--"
|
||||||
// 1048576 = 1024 * 1024
|
// 1048576 = 1024 * 1024
|
||||||
|
|||||||
@@ -115,3 +115,59 @@ export interface SubmissionListPayload {
|
|||||||
limit: number
|
limit: number
|
||||||
offset: number
|
offset: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Rank {
|
||||||
|
id: number
|
||||||
|
user: {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
real_name: null
|
||||||
|
}
|
||||||
|
acm_problems_status: {
|
||||||
|
problems: {
|
||||||
|
[key: string]: {
|
||||||
|
_id: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contest_problems?: {
|
||||||
|
[key: string]: {
|
||||||
|
[key: string]: {
|
||||||
|
_id: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oi_problems_status: {}
|
||||||
|
real_name: null | string
|
||||||
|
avatar: string
|
||||||
|
blog: null
|
||||||
|
mood: null | string
|
||||||
|
github: null
|
||||||
|
school: null | string
|
||||||
|
major: null | string
|
||||||
|
language: null | string
|
||||||
|
accepted_number: number
|
||||||
|
total_score: number
|
||||||
|
submission_number: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Contest {
|
||||||
|
id: number
|
||||||
|
created_by: {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
real_name: null
|
||||||
|
}
|
||||||
|
status: "-1" | "0" | "1"
|
||||||
|
contest_type: "Password Protected" | "Public"
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
real_time_rank: boolean
|
||||||
|
rule_type: "ACM"
|
||||||
|
start_time: Date
|
||||||
|
end_time: Date
|
||||||
|
create_time: Date
|
||||||
|
last_update_time: Date
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user