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",
|
||||
"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",
|
||||
|
||||
@@ -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
1
src/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
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 { 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user