contest list.

This commit is contained in:
2023-01-23 16:20:37 +08:00
parent f75ae1b00d
commit 8e05cb601d
14 changed files with 360 additions and 31 deletions

1
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@vueuse/integrations": "^9.11.0",
"axios": "1.2.2",
"copy-text-to-clipboard": "^3.0.1",
"date-fns": "^2.29.3",
"highlight.js": "^11.7.0",
"naive-ui": "^2.34.3",
"party-js": "^2.2.0",

View File

@@ -16,6 +16,7 @@
"@vueuse/integrations": "^9.11.0",
"axios": "1.2.2",
"copy-text-to-clipboard": "^3.0.1",
"date-fns": "^2.29.3",
"highlight.js": "^11.7.0",
"naive-ui": "^2.34.3",
"party-js": "^2.2.0",

1
src/components.d.ts vendored
View File

@@ -10,6 +10,7 @@ declare module '@vue/runtime-core' {
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']
NAlert: typeof import('naive-ui')['NAlert']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']

View File

@@ -92,3 +92,18 @@ export function adminRejudge(id: string) {
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 })
}

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

View File

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

View File

@@ -8,7 +8,7 @@ import { submissionMemoryFormat, submissionTimeFormat } from "utils/functions"
import { Problem, Submission, SubmitCodePayload } from "utils/types"
import { getSubmission, submitCode } from "oj/api"
import SubmissionResultTag from "../../../shared/SubmissionResultTag.vue"
import { DataTableColumn } from "naive-ui"
import type { DataTableColumn } from "naive-ui"
const problem = inject<Ref<Problem>>("problem")
@@ -235,9 +235,9 @@ const tabProps = {
<n-data-table
v-if="infoTable.length"
size="small"
striped
:data="infoTable"
:columns="columns"
striped
/>
</n-scrollbar>
</n-tab-pane>

View File

@@ -1,11 +1,72 @@
<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>
<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>
<style scoped></style>

View File

@@ -54,7 +54,7 @@ onMounted(init)
<n-space>
<span>提交时间{{ parseTime(submission.create_time) }}</span>
<span>语言{{ submission.language }}</span>
<span>提交人 {{ submission.username }}</span>
<span>用户 {{ submission.username }}</span>
</n-space>
</n-alert>
<n-card embedded>

View File

@@ -172,7 +172,7 @@ const columns = computed(() => {
},
{ title: "语言", key: "language", width: 100 },
{
title: "提交者",
title: "用户",
key: "username",
minWidth: 120,
render: (row) =>
@@ -206,7 +206,7 @@ const columns = computed(() => {
<n-form-item label="只看自己">
<n-switch v-model:value="query.myself" />
</n-form-item>
<n-form-item label="搜索提交者">
<n-form-item label="搜索用户">
<n-input @change="search" clearable placeholder="输入后回车" />
</n-form-item>
<n-form-item>

View File

@@ -13,7 +13,6 @@ export const routes = [
{
path: "submission",
component: () => import("oj/submission/list.vue"),
meta: { requiresAuth: true },
},
{
path: "submission/:submissionID",
@@ -23,7 +22,6 @@ export const routes = [
{
path: "contest",
component: () => import("oj/contest/list.vue"),
meta: { requiresAuth: true },
},
{
path: "contest/:contestID",

View File

@@ -70,32 +70,26 @@ export const JUDGE_STATUS: {
},
}
export const CONTEST_STATUS = {
NOT_START: "1",
UNDERWAY: "0",
ENDED: "-1",
}
export const CONTEST_STATUS_REVERSE = {
export const CONTEST_STATUS: {
[key in "1" | "-1" | "0"]: {
name: string
type: "error" | "success" | "warning"
}
} = {
"1": {
name: "Not Started",
color: "yellow",
name: "未开始",
type: "warning",
},
"0": {
name: "Underway",
color: "green",
name: "进行中",
type: "success",
},
"-1": {
name: "Ended",
color: "red",
name: "已结束",
type: "error",
},
}
export const RULE_TYPE = {
ACM: "ACM",
OI: "OI",
}
export const CONTEST_TYPE = {
PUBLIC: "Public",
PRIVATE: "Password Protected",

View File

@@ -1,3 +1,4 @@
import { intervalToDuration } from "date-fns"
import { STORAGE_KEY } from "./constants"
export function getACRate(acCount: number, totalCount: number) {
@@ -40,6 +41,30 @@ export function parseTime(utc: Date, format = "YYYY年M月D日") {
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) {
if (memory === undefined) return "--"
// 1048576 = 1024 * 1024

View File

@@ -115,3 +115,59 @@ export interface SubmissionListPayload {
limit: 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
}