This commit is contained in:
2023-04-04 11:46:28 +08:00
parent ae621b7dd2
commit 2066cb441b
18 changed files with 210 additions and 105 deletions

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { useContestStore } from "oj/store/contest" import { useContestStore } from "oj/store/contest"
import ContestTypeVue from "~/shared/ContestType.vue" import ContestType from "~/shared/ContestType.vue"
const contestStore = useContestStore() const contestStore = useContestStore()
</script> </script>
@@ -27,7 +27,7 @@ const contestStore = useContestStore()
{{ parseTime(contestStore.contest.end_time, "YYYY年M月D日 hh:mm:ss") }} {{ parseTime(contestStore.contest.end_time, "YYYY年M月D日 hh:mm:ss") }}
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="比赛类型"> <n-descriptions-item label="比赛类型">
<ContestTypeVue :contest="contestStore.contest" /> <ContestType :contest="contestStore.contest" />
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="发起人"> <n-descriptions-item label="发起人">
{{ contestStore.contest.created_by.username }} {{ contestStore.contest.created_by.username }}

View File

@@ -29,14 +29,17 @@ const passwordFormVisible = computed(
</script> </script>
<template> <template>
<div v-if="contestStore.contest"> <n-space vertical v-if="contestStore.contest">
<n-space class="title" align="center" justify="space-between"> <n-space align="center" justify="space-between">
<n-space align="center"> <n-space align="center">
<h2 class="contestTitle">{{ contestStore.contest.title }}</h2> <h2 class="contestTitle">{{ contestStore.contest.title }}</h2>
<n-icon size="large" v-if="contestStore.isPrivate" class="lockIcon"> <n-icon size="large" v-if="contestStore.isPrivate" class="lockIcon">
<i-ep-lock /> <i-ep-lock />
</n-icon> </n-icon>
<n-tag :type="CONTEST_STATUS[contestStore.contestStatus]['type']"> <n-tag
size="small"
:type="CONTEST_STATUS[contestStore.contestStatus]['type']"
>
{{ contestStore.countdown }} {{ contestStore.countdown }}
</n-tag> </n-tag>
</n-space> </n-space>
@@ -67,14 +70,10 @@ const passwordFormVisible = computed(
</n-form-item> </n-form-item>
</n-form> </n-form>
<router-view></router-view> <router-view></router-view>
</div> </n-space>
</template> </template>
<style scoped> <style scoped>
.title {
margin-bottom: 16px;
}
.contestTitle { .contestTitle {
font-weight: 500; font-weight: 500;
margin: 0; margin: 0;

View File

@@ -119,31 +119,29 @@ function rowProps(row: Contest) {
} }
</script> </script>
<template> <template>
<n-form label-placement="left" :inline="isDesktop"> <n-space>
<n-form-item label="状态"> <n-form :show-feedback="false" label-placement="left" inline>
<n-select <n-form-item label="比赛状态">
class="select" <n-select
:options="options" class="select"
v-model:value="query.status" :options="options"
/> v-model:value="query.status"
</n-form-item> />
<n-form-item label="搜索比赛标题"> </n-form-item>
<n-input placeholder="输入后回车或点击搜索" clearable @change="search" /> <n-form-item label="搜索">
</n-form-item> <n-input clearable @change="search" />
<n-form-item> </n-form-item>
<n-space> </n-form>
<n-button @click="search(query.keyword)">搜索</n-button> <n-form label-placement="left" inline>
<n-button @click="clear">重置</n-button> <n-form-item>
</n-space> <n-space>
</n-form-item> <n-button @click="search(query.keyword)">搜索</n-button>
</n-form> <n-button @click="clear">重置</n-button>
<n-data-table </n-space>
size="small" </n-form-item>
striped </n-form>
:columns="columns" </n-space>
:data="data" <n-data-table striped :columns="columns" :data="data" :row-props="rowProps" />
:row-props="rowProps"
/>
<Pagination <Pagination
v-model:limit="query.limit" v-model:limit="query.limit"
v-model:page="query.page" v-model:page="query.page"

View File

@@ -34,7 +34,6 @@ function rowProps(row: ProblemFiltered) {
<template> <template>
<n-data-table <n-data-table
striped striped
size="small"
:data="contestStore.problems" :data="contestStore.problems"
:columns="problemsColumns" :columns="problemsColumns"
:row-props="rowProps" :row-props="rowProps"

View File

@@ -165,7 +165,6 @@ onMounted(() => {
striped striped
:single-line="false" :single-line="false"
:scroll-x="1200" :scroll-x="1200"
size="small"
:columns="columns" :columns="columns"
:data="data" :data="data"
/> />

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { Promotion, CloseBold, Select } from "@element-plus/icons-vue"
import Copy from "~/shared/Copy.vue" import Copy from "~/shared/Copy.vue"
import { code } from "oj/composables/code" import { code } from "oj/composables/code"
import { SOURCES } from "utils/constants" import { SOURCES } from "utils/constants"
@@ -78,18 +77,22 @@ async function test(sample: Sample, index: number) {
}) })
} }
const icon = (status: ProblemStatus) => function label(status: ProblemStatus, loading: boolean) {
({ if (loading) return "测试中"
not_test: Promotion, return {
failed: CloseBold, not_test: "测试",
passed: Select, failed: "不通过",
}[status]) passed: "通过",
const type = (status: ProblemStatus) => }[status]
({ }
function type(status: ProblemStatus) {
return {
not_test: "", not_test: "",
failed: "error", failed: "error",
passed: "success", passed: "success",
}[status] as "warning" | "error" | "success") }[status] as "warning" | "error" | "success"
}
</script> </script>
<template> <template>
@@ -120,15 +123,12 @@ const type = (status: ProblemStatus) =>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
<n-button <n-button
size="small"
:type="type(sample.status)" :type="type(sample.status)"
:disabled="disabled" :disabled="disabled"
:loading="sample.loading"
circle
@click="test(sample, index)" @click="test(sample, index)"
> >
<template #icon> {{ label(sample.status, sample.loading) }}
<component :is="icon(sample.status)"></component>
</template>
</n-button> </n-button>
</template> </template>
点击测试 点击测试

View File

@@ -65,7 +65,7 @@ const options = {
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item :span="3" label="标签"> <n-descriptions-item :span="3" label="标签">
<n-space> <n-space>
<n-tag type="info" v-for="tag in problem.tags" :key="tag"> <n-tag size="small" type="info" v-for="tag in problem.tags" :key="tag">
{{ tag }} {{ tag }}
</n-tag> </n-tag>
</n-space> </n-space>

View File

@@ -227,7 +227,6 @@ watch(
<n-card v-if="msg" embedded class="msg">{{ msg }}</n-card> <n-card v-if="msg" embedded class="msg">{{ msg }}</n-card>
<n-data-table <n-data-table
v-if="infoTable.length" v-if="infoTable.length"
size="small"
striped striped
:data="infoTable" :data="infoTable"
:columns="columns" :columns="columns"

View File

@@ -2,7 +2,6 @@
import { useUserStore } from "~/shared/store/user" import { useUserStore } from "~/shared/store/user"
import { filterEmptyValue, getTagColor } from "utils/functions" import { filterEmptyValue, getTagColor } from "utils/functions"
import { ProblemFiltered } from "utils/types" import { ProblemFiltered } from "utils/types"
import { isMobile } from "~/shared/composables/breakpoints"
import { getProblemList, getRandomProblemID } from "oj/api" import { getProblemList, getRandomProblemID } from "oj/api"
import Pagination from "~/shared/Pagination.vue" import Pagination from "~/shared/Pagination.vue"
import { DataTableColumn, NSpace, NTag } from "naive-ui" import { DataTableColumn, NSpace, NTag } from "naive-ui"
@@ -169,8 +168,8 @@ function rowProps(row: ProblemFiltered) {
</script> </script>
<template> <template>
<n-space :vertical="isMobile"> <n-space>
<n-form inline label-placement="left"> <n-form :show-feedback="false" inline label-placement="left">
<n-form-item label="难度"> <n-form-item label="难度">
<n-select <n-select
class="select" class="select"
@@ -178,12 +177,8 @@ function rowProps(row: ProblemFiltered) {
:options="difficultyOptions" :options="difficultyOptions"
/> />
</n-form-item> </n-form-item>
<n-form-item> <n-form-item label="搜索">
<n-input <n-input clearable @change="search" />
placeholder="输入编号或标题后回车"
clearable
@change="search"
/>
</n-form-item> </n-form-item>
</n-form> </n-form>
<n-form inline label-placement="left"> <n-form inline label-placement="left">
@@ -212,7 +207,6 @@ function rowProps(row: ProblemFiltered) {
<n-data-table <n-data-table
class="table" class="table"
striped striped
size="small"
:data="problems" :data="problems"
:columns="columns" :columns="columns"
:row-props="rowProps" :row-props="rowProps"

View File

@@ -69,6 +69,7 @@ const data = computed(() => ({
})) }))
const options = { const options = {
maintainAspectRatio: false,
plugins: { plugins: {
title: { title: {
text: "全校前十名的用户(不包括超管)", text: "全校前十名的用户(不包括超管)",
@@ -79,10 +80,13 @@ const options = {
} }
</script> </script>
<template> <template>
<Bar class="chart" :data="data" :options="options" /> <div class="chart">
<Bar :data="data" :options="options" />
</div>
</template> </template>
<style scoped> <style scoped>
.chart { .chart {
margin-bottom: 24px; height: 400px;
margin-bottom: 20px;
} }
</style> </style>

View File

@@ -73,7 +73,7 @@ onMounted(listRanks)
<template> <template>
<Chart v-if="!!chart.length" :rankData="chart" /> <Chart v-if="!!chart.length" :rankData="chart" />
<n-data-table striped size="small" :data="data" :columns="columns" /> <n-data-table striped :data="data" :columns="columns" />
<Pagination <Pagination
:total="total" :total="total"
v-model:page="query.page" v-model:page="query.page"

View File

@@ -67,7 +67,12 @@ onMounted(init)
</n-alert> </n-alert>
<n-card embedded> <n-card embedded>
<n-space justify="end"> <n-space justify="end">
<n-button type="primary" @click="handleCopy(submission!.code)"> <n-button
quaternary
class="copyBtn"
type="primary"
@click="handleCopy(submission!.code)"
>
{{ copied ? "已复制" : "复制代码" }} {{ copied ? "已复制" : "复制代码" }}
</n-button> </n-button>
</n-space> </n-space>
@@ -90,4 +95,8 @@ onMounted(init)
.code { .code {
font-size: 20px; font-size: 20px;
} }
.copyBtn {
margin-bottom: 16px;
}
</style> </style>

View File

@@ -128,6 +128,7 @@ const columns = computed(() => {
{ {
title: "编号", title: "编号",
key: "id", key: "id",
minWidth: 160,
render: (row) => { render: (row) => {
if (row.show_link) { if (row.show_link) {
return h( return h(
@@ -215,6 +216,7 @@ const columns = computed(() => {
h( h(
NButton, NButton,
{ {
quaternary: true,
size: "small", size: "small",
type: "primary", type: "primary",
onClick: () => rejudge(row.id), onClick: () => rejudge(row.id),
@@ -227,28 +229,32 @@ const columns = computed(() => {
}) })
</script> </script>
<template> <template>
<n-form :inline="isDesktop" label-placement="left"> <n-space>
<n-form-item label="提交状态"> <n-form :show-feedback="false" inline label-placement="left">
<n-select <n-form-item label="提交状态">
class="select" <n-select
v-model:value="query.result" class="select"
:options="options" v-model:value="query.result"
/> :options="options"
</n-form-item> />
<n-form-item label="只看自己"> </n-form-item>
<n-switch v-model:value="query.myself" /> <n-form-item label="只看自己">
</n-form-item> <n-switch v-model:value="query.myself" />
<n-form-item label="搜索用户"> </n-form-item>
<n-input @change="search" clearable placeholder="输入后回车或点击搜索" /> </n-form>
</n-form-item> <n-form inline label-placement="left">
<n-form-item> <n-form-item label="搜索用户">
<n-space> <n-input clearable @change="search" />
<n-button @click="search(query.username)">搜索</n-button> </n-form-item>
<n-button @click="clear">重置</n-button> <n-form-item>
</n-space> <n-space>
</n-form-item> <n-button @click="search(query.username)">搜索</n-button>
</n-form> <n-button @click="clear">重置</n-button>
<n-data-table striped size="small" :columns="columns" :data="submissions" /> </n-space>
</n-form-item>
</n-form>
</n-space>
<n-data-table striped :columns="columns" :data="submissions" />
<Pagination <Pagination
:total="total" :total="total"
v-model:limit="query.limit" v-model:limit="query.limit"

View File

@@ -6,7 +6,7 @@ import type { FormRules } from "naive-ui"
const userStore = useUserStore() const userStore = useUserStore()
const loginRef = ref() const loginRef = ref()
const [isLoading] = useToggle() const [isLoading, toggleLoading] = useToggle()
const msg = ref("") const msg = ref("")
const form = reactive({ const form = reactive({
username: "", username: "",
@@ -21,11 +21,11 @@ const rules: FormRules = {
} }
async function submit() { async function submit() {
loginRef.value?.validate(async (errors: FormRules | undefined) => { loginRef.value!.validate(async (errors: FormRules | undefined) => {
if (!errors) { if (!errors) {
try { try {
msg.value = "" msg.value = ""
isLoading.value = true toggleLoading(true)
await login(form) await login(form)
} catch (err: any) { } catch (err: any) {
if (err.data === "Your account has been disabled") { if (err.data === "Your account has been disabled") {
@@ -36,7 +36,7 @@ async function submit() {
msg.value = "无法登录" msg.value = "无法登录"
} }
} finally { } finally {
isLoading.value = false toggleLoading(false)
} }
if (!msg.value) { if (!msg.value) {
toggleLogin(false) toggleLogin(false)
@@ -85,7 +85,7 @@ function goSignup() {
<n-button type="primary" :loading="isLoading" @click="submit"> <n-button type="primary" :loading="isLoading" @click="submit">
登录 登录
</n-button> </n-button>
<n-button @click="goSignup">没有账号立即注册</n-button> <n-button @click="goSignup">没有账号立即注册</n-button>
</n-space> </n-space>
</n-form-item> </n-form-item>
</n-form> </n-form>

View File

@@ -41,7 +41,7 @@ watch(page, () => emit("update:page", page))
</template> </template>
<style scoped> <style scoped>
.margin { .margin {
margin-top: 24px; margin: 20px 0;
} }
.right { .right {
float: right; float: right;

View File

@@ -1,16 +1,43 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormRules } from "naive-ui" import { getCaptcha, signup, login } from "./api"
import { signupModal, toggleLogin, toggleSignup } from "./composables/modal" import { signupModal, toggleLogin, toggleSignup } from "./composables/modal"
import { useUserStore } from "./store/user"
import type { FormItemRule, FormRules } from "naive-ui"
const userStore = useUserStore()
const signupRef = ref()
const captchaSrc = ref("")
const form = reactive({ const form = reactive({
username: "", username: "",
email: "",
password: "", password: "",
passwordAgain: "", passwordAgain: "",
email: "", captcha: "",
}) })
const rules: FormRules = {}
const [isLoading] = useToggle() const rules: FormRules = {
username: [{ required: true, message: "用户名必填", trigger: "blur" }],
email: [{ required: true, message: "邮箱必填", trigger: "blur" }],
password: [
{ required: true, message: "密码必填", trigger: "blur" },
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
],
passwordAgain: [
{ required: true, message: "密码必填", trigger: "blur" },
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
{
validator: (_: FormItemRule, value: string) => value === form.password,
message: "两次密码输入不一致",
trigger: "blur",
},
],
captcha: [
{ required: true, message: "验证码必填", trigger: "blur", min: 1, max: 10 },
],
}
const [isLoading, toggleLoading] = useToggle()
const msg = ref("") const msg = ref("")
function goLogin() { function goLogin() {
@@ -18,7 +45,50 @@ function goLogin() {
toggleSignup(false) toggleSignup(false)
} }
function submit() {} function submit() {
signupRef.value!.validate(async (errors: FormRules | undefined) => {
if (!errors) {
try {
msg.value = ""
toggleLoading(true)
await signup({
username: form.username,
email: form.email,
password: form.password,
captcha: form.captcha,
})
} catch (err: any) {
if (err.data === "Invalid captcha") {
msg.value = "验证码不正确"
} else if (err.data === "Username already exists") {
msg.value = "用户名已存在"
} else if (err.data === "Email already exists") {
msg.value = "邮箱已存在"
} else {
msg.value = "无法注册"
}
getCaptchaSrc()
form.captcha = ""
} finally {
toggleLoading(false)
}
if (!msg.value) {
toggleSignup(false)
await login({ username: form.username, password: form.password })
userStore.getMyProfile()
}
}
})
}
async function getCaptchaSrc() {
const res = await getCaptcha()
captchaSrc.value = res.data
}
watch(signupModal, (v) => {
if (v) getCaptchaSrc()
})
</script> </script>
<template> <template>
@@ -55,7 +125,7 @@ function submit() {}
name="signup password" name="signup password"
/> />
</n-form-item> </n-form-item>
<n-form-item label="确认密码" path="password"> <n-form-item label="确认密码" path="passwordAgain">
<n-input <n-input
v-model:value="form.passwordAgain" v-model:value="form.passwordAgain"
clearable clearable
@@ -63,11 +133,21 @@ function submit() {}
name="signup password again" name="signup password again"
/> />
</n-form-item> </n-form-item>
<n-form-item label="验证码" path="captcha">
<n-space>
<n-input
v-model:value="form.captcha"
clearable
name="signup captcha"
/>
<img class="captcha" :src="captchaSrc" @click="getCaptchaSrc" />
</n-space>
</n-form-item>
<n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert> <n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert>
<n-form-item> <n-form-item>
<n-space> <n-space>
<n-button type="primary" :loading="isLoading" @click="submit"> <n-button type="primary" :loading="isLoading" @click="submit">
登录 注册
</n-button> </n-button>
<n-button @click="goLogin">已经注册现在登录</n-button> <n-button @click="goLogin">已经注册现在登录</n-button>
</n-space> </n-space>
@@ -76,4 +156,9 @@ function submit() {}
</n-modal> </n-modal>
</template> </template>
<style scoped></style> <style scoped>
.captcha {
height: 34px;
cursor: pointer;
}
</style>

View File

@@ -5,6 +5,15 @@ export function login(data: { username: string; password: string }) {
return http.post("login", data) return http.post("login", data)
} }
export function signup(data: {
username: string
email: string
password: string
captcha: string
}) {
return http.post("register", data)
}
export function logout() { export function logout() {
return http.get("logout") return http.get("logout")
} }
@@ -16,3 +25,7 @@ export function getProfile(username: string = "") {
export function getProblemTagList() { export function getProblemTagList() {
return http.get<Tag[]>("problem/tags") return http.get<Tag[]>("problem/tags")
} }
export function getCaptcha() {
return http.get("captcha")
}

View File

@@ -5,7 +5,7 @@ import Header from "../Header.vue"
</script> </script>
<template> <template>
<n-layout> <n-layout position="absolute">
<n-layout-header bordered class="header"> <n-layout-header bordered class="header">
<Header /> <Header />
</n-layout-header> </n-layout-header>