remove shared components.

This commit is contained in:
2023-11-21 23:06:57 +08:00
parent ab985ff65f
commit aa7d46effc
33 changed files with 53 additions and 43 deletions

View File

@@ -0,0 +1,68 @@
<script lang="ts" setup>
import { Codemirror } from "vue-codemirror"
import { cpp } from "@codemirror/lang-cpp"
import { python } from "@codemirror/lang-python"
import { EditorView } from "@codemirror/view"
import { oneDark } from "../themes/oneDark"
import { smoothy } from "../themes/smoothy"
import { LANGUAGE } from "~/utils/types"
import { isDark } from "../composables/dark"
const styleTheme = EditorView.baseTheme({
"& .cm-scroller": {
"font-family": "Consolas",
},
"&.cm-editor.cm-focused": {
outline: "none",
},
})
interface Props {
modelValue: string
language?: LANGUAGE
fontSize?: number
height?: string
readonly?: boolean
placeholder?: string
}
const props = withDefaults(defineProps<Props>(), {
language: "C",
fontSize: 20,
height: "100%",
readonly: false,
placeholder: "",
})
const code = ref(props.modelValue)
watch(
() => props.modelValue,
(v) => {
code.value = v
},
)
const emit = defineEmits(["update:modelValue"])
const lang = computed(() => {
if (props.language === "Python3" || props.language === "Python2") {
return python()
}
return cpp()
})
function onChange(v: string) {
emit("update:modelValue", v)
}
</script>
<template>
<Codemirror
v-model="code"
indentWithTab
:extensions="[styleTheme, lang, isDark ? oneDark : smoothy]"
:disabled="props.readonly"
:tabSize="4"
:placeholder="props.placeholder"
:style="{ height: props.height, fontSize: props.fontSize + 'px' }"
@change="onChange"
/>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { Contest } from "utils/types"
import { ContestType } from "utils/constants"
defineProps<{ contest: Contest }>()
</script>
<template>
<n-space>
<span>{{ contest.title }}</span>
<n-icon
size="medium"
class="lockIcon"
v-if="contest.contest_type === ContestType.private"
>
<i-ep-lock />
</n-icon>
</n-space>
</template>
<style scoped>
.lockIcon {
transform: translateY(2px);
}
</style>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { Contest } from "~/utils/types"
import { ContestType } from "~/utils/constants"
interface Props {
contest: Contest
size?: "small"
}
const props = defineProps<Props>()
const isPrivate = computed(
() => props.contest.contest_type === ContestType.private,
)
</script>
<template>
<n-tag :type="isPrivate ? 'error' : 'info'" :size="props.size">
{{ isPrivate ? "需要密码" : "公开" }}
</n-tag>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { DocumentCopy, Select } from "@element-plus/icons-vue"
import copy from "copy-text-to-clipboard"
defineProps<{ value: string }>()
const [copied, toggle] = useToggle()
const { start } = useTimeoutFn(() => toggle(false), 1000, { immediate: false })
function handleClick(value: string) {
copy(value)
toggle(true)
start()
}
</script>
<template>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon class="icon" @click="handleClick(value)">
<component :is="copied ? Select : DocumentCopy"></component>
</n-icon>
</template>
{{ copied ? "已复制" : "复制" }}
</n-tooltip>
</template>
<style scoped>
.icon {
cursor: pointer;
transform: translateY(2px);
}
</style>

View File

@@ -0,0 +1,183 @@
<script setup lang="ts">
import { logout } from "../api"
import { useUserStore } from "../store/user"
import { useConfigStore } from "../store/config"
import { isDark, toggleDark } from "~/shared/composables/dark"
import { toggleLogin, toggleSignup } from "~/shared/composables/modal"
import { RouterLink } from "vue-router"
import { isDesktop, isMobile } from "~/shared/composables/breakpoints"
import {
screenSwitchLabel,
switchScreenMode,
} from "~/shared/composables/switchScreen"
import { code } from "~/shared/composables/learn"
import { useLearnStore } from "~/learn/store"
const userStore = useUserStore()
const configStore = useConfigStore()
const learnStore = useLearnStore()
const route = useRoute()
const router = useRouter()
const active = computed(() => {
const path = route.path.split("/")[1] || "problem"
return !["user", "setting"].includes(path) ? path : ""
})
const hiddenTitle = computed(() => isMobile.value && route.name === "learn")
async function handleLogout() {
await logout()
userStore.clearProfile()
router.replace("/")
}
onMounted(() => {
userStore.getMyProfile()
configStore.getConfig()
})
const menus = computed<MenuOption[]>(() => [
{
label: () =>
h(RouterLink, { to: "/learn/step-1" }, { default: () => "自学" }),
key: "learn",
show: false,
},
{
label: () => h(RouterLink, { to: "/" }, { default: () => "题库" }),
key: "problem",
},
{
label: () => h(RouterLink, { to: "/contest" }, { default: () => "比赛" }),
key: "contest",
},
{
label: () =>
h(RouterLink, { to: "/submission" }, { default: () => "提交" }),
key: "submission",
},
{
label: () => h(RouterLink, { to: "/rank" }, { default: () => "排名" }),
key: "rank",
},
{
label: () => h(RouterLink, { to: "/admin" }, { default: () => "后台" }),
show: userStore.isAdminRole,
key: "admin",
},
])
const options: Array<DropdownOption | DropdownDividerOption> = [
{
label: "我的主页",
key: "home",
props: {
onClick: () => router.push("/user"),
},
},
{
label: "我的提交",
key: "status",
props: {
onClick: () => router.push("/submission?myself=1"),
},
},
{
label: "我的设置",
key: "setting",
props: {
onClick: () => router.push("/setting"),
},
},
{ type: "divider" },
{ label: "退出", key: "logout", props: { onClick: handleLogout } },
]
function run() {
console.log(code.value)
}
function goHome() {
router.push("/")
}
function switchScreen() {}
</script>
<template>
<n-space justify="space-between" align="center">
<n-space align="center">
<div v-if="!hiddenTitle" class="websiteTitle" @click="goHome">
{{ configStore.config?.website_name }}
</div>
<n-menu
v-if="isDesktop"
mode="horizontal"
:options="menus"
:value="active"
/>
</n-space>
<n-space align="center">
<n-dropdown
v-if="route.name === 'learn' && isMobile"
trigger="click"
:options="learnStore.menu"
>
<n-button>目录</n-button>
</n-dropdown>
<div v-if="route.name === 'learn'">
<n-button v-if="isDesktop" type="primary" @click="run">
运行代码
</n-button>
<n-button v-else circle @click="run">
<n-icon>
<i-ep-arrow-right-bold />
</n-icon>
</n-button>
</div>
<n-dropdown v-if="isMobile" :options="menus" trigger="click">
<n-button>菜单</n-button>
</n-dropdown>
<n-button
v-if="
isDesktop &&
(route.name === 'problem' || route.name === 'contest problem')
"
@click="switchScreenMode"
>
{{ screenSwitchLabel }}
</n-button>
<div v-if="userStore.isFinished">
<n-dropdown
v-if="userStore.isAuthed"
:options="options"
trigger="click"
>
<n-button>{{ userStore.user!.username }}</n-button>
</n-dropdown>
<n-space align="center" v-else>
<n-button @click="toggleLogin(true)">登录</n-button>
<n-button
v-if="configStore.config?.allow_register"
@click="toggleSignup(true)"
>
注册
</n-button>
</n-space>
</div>
<n-button circle @click="toggleDark()">
<template #icon>
<n-icon v-if="isDark"><i-ep-sunny /></n-icon>
<n-icon v-else> <i-ep-moon /></n-icon>
</template>
</n-button>
</n-space>
</n-space>
</template>
<style scoped>
.websiteTitle {
font-size: 18px;
margin-left: 8px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import { login } from "../api"
import { loginModal, toggleLogin, toggleSignup } from "../composables/modal"
import { useUserStore } from "../store/user"
const userStore = useUserStore()
const loginRef = ref()
const [isLoading, toggleLoading] = useToggle()
const msg = ref("")
const form = reactive({
username: "",
password: "",
})
const rules: FormRules = {
username: [{ required: true, message: "用户名必填", trigger: "blur" }],
password: [
{ required: true, message: "密码必填", trigger: "blur" },
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
],
}
async function submit() {
loginRef.value!.validate(async (errors: FormRules | undefined) => {
if (!errors) {
try {
msg.value = ""
toggleLoading(true)
await login(form)
} catch (err: any) {
if (err.data === "Your account has been disabled") {
msg.value = "此账号已被封禁"
} else if (err.data === "Invalid username or password") {
msg.value = "用户名或密码不正确"
} else {
msg.value = "无法登录"
}
} finally {
toggleLoading(false)
}
if (!msg.value) {
toggleLogin(false)
userStore.getMyProfile()
}
}
})
}
function goSignup() {
toggleLogin(false)
toggleSignup(true)
}
</script>
<template>
<n-modal
:mask-closable="false"
v-model:show="loginModal"
preset="card"
title="登录"
style="width: 400px"
:auto-focus="false"
>
<n-form ref="loginRef" :model="form" :rules="rules" show-require-mark>
<n-form-item label="用户名" path="username">
<n-input
v-model:value="form.username"
autofocus
clearable
name="login username"
/>
</n-form-item>
<n-form-item label="密码" path="password">
<n-input
v-model:value="form.password"
clearable
type="password"
name="login password"
@change="submit"
/>
</n-form-item>
<n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert>
<n-form-item>
<n-space>
<n-button type="primary" :loading="isLoading" @click="submit">
登录
</n-button>
<n-button @click="goSignup">没有账号立即注册</n-button>
</n-space>
</n-form-item>
</n-form>
</n-modal>
</template>
<style scoped></style>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { isDesktop } from "~/shared/composables/breakpoints"
interface Props {
total: number
limit: number
page: number
}
const props = withDefaults(defineProps<Props>(), {
limit: 10,
page: 1,
})
const emit = defineEmits(["update:limit", "update:page"])
const route = useRoute()
const limit = ref(props.limit)
const page = ref(props.page)
const sizes = computed(() => {
if (route.name === "contest rank") return [10, 30, 50]
return [10, 20, 30]
})
watch(limit, () => emit("update:limit", limit))
watch(page, () => emit("update:page", page))
</script>
<template>
<n-pagination
v-if="props.total"
class="right margin"
:item-count="props.total"
v-model:page="page"
v-model:page-size="limit"
:page-sizes="sizes"
:page-slot="isDesktop ? 7 : 5"
show-size-picker
/>
</template>
<style scoped>
.margin {
margin: 20px 0;
}
.right {
float: right;
}
</style>

View File

@@ -0,0 +1,163 @@
<script setup lang="ts">
import { getCaptcha, signup, login } from "../api"
import { signupModal, toggleLogin, toggleSignup } from "../composables/modal"
import { useUserStore } from "../store/user"
const userStore = useUserStore()
const signupRef = ref()
const captchaSrc = ref("")
const form = reactive({
username: "",
email: "",
password: "",
passwordAgain: "",
captcha: "",
})
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("")
function goLogin() {
toggleLogin(true)
toggleSignup(false)
}
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>
<template>
<n-modal
:mask-closable="false"
v-model:show="signupModal"
preset="card"
title="注册"
style="width: 400px"
:auto-focus="false"
>
<n-form ref="signupRef" :model="form" :rules="rules" show-require-mark>
<n-form-item label="用户名" path="username">
<n-input
v-model:value="form.username"
autofocus
clearable
name="signup username"
/>
</n-form-item>
<n-form-item label="邮箱" path="email">
<n-input
v-model:value="form.email"
clearable
name="signup email"
@change="submit"
/>
</n-form-item>
<n-form-item label="密码" path="password">
<n-input
v-model:value="form.password"
clearable
type="password"
name="signup password"
/>
</n-form-item>
<n-form-item label="确认密码" path="passwordAgain">
<n-input
v-model:value="form.passwordAgain"
clearable
type="password"
name="signup password again"
/>
</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-form-item>
<n-space>
<n-button type="primary" :loading="isLoading" @click="submit">
注册
</n-button>
<n-button @click="goLogin">已经注册现在登录</n-button>
</n-space>
</n-form-item>
</n-form>
</n-modal>
</template>
<style scoped>
.captcha {
height: 34px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { JUDGE_STATUS } from "utils/constants"
import { SUBMISSION_RESULT } from "utils/types"
interface Props {
result: SUBMISSION_RESULT
}
defineProps<Props>()
</script>
<template>
<n-tag :type="JUDGE_STATUS[result]['type']">
{{ JUDGE_STATUS[result]["name"] }}
</n-tag>
</template>
<style scoped></style>

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import "@wangeditor/editor/dist/css/style.css"
import { IDomEditor, IEditorConfig, IToolbarConfig } from "@wangeditor/editor"
import { Editor, Toolbar } from "@wangeditor/editor-for-vue"
import { uploadImage } from "../../admin/api"
interface Props {
value: string
title: string
minHeight?: number
}
type InsertFnType = (url: string, alt: string, href: string) => void
const props = withDefaults(defineProps<Props>(), {
minHeight: 0,
})
const emit = defineEmits(["update:value"])
const message = useMessage()
const rawHtml = ref(props.value)
watch(rawHtml, () => emit("update:value", rawHtml.value))
const editorRef = shallowRef<IDomEditor>()
const toolbarConfig: Partial<IToolbarConfig> = {
excludeKeys: ["todo", "insertVideo", "fullScreen"],
}
const editorConfig: Partial<IEditorConfig> = {
scroll: false,
MENU_CONF: {
uploadImage: { customUpload },
},
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor) editor.destroy()
})
function onClick() {
if (!editorRef.value) return
editorRef.value.blur()
editorRef.value.focus()
}
function handleCreated(editor: IDomEditor) {
editorRef.value = editor
}
async function customUpload(file: File, insertFn: InsertFnType) {
const path = await uploadImage(file)
if (!path) {
message.error("图片上传失败")
return
}
const url = path
const alt = "图片"
const href = ""
insertFn(url, alt, href)
}
</script>
<template>
<div class="title" v-if="props.title">{{ props.title }}</div>
<div class="editorWrapper">
<Toolbar
class="toolbar"
:editor="editorRef"
:defaultConfig="toolbarConfig"
mode="simple"
/>
<Editor
@click="onClick"
:style="{ minHeight: props.minHeight + 'px' }"
v-model="rawHtml"
:defaultConfig="editorConfig"
mode="simple"
@onCreated="handleCreated"
/>
</div>
</template>
<style scoped>
.title {
margin-bottom: 12px;
}
.toolbar {
border-bottom: 1px solid #ddd;
}
.editorWrapper {
border: 1px solid #ddd;
margin-bottom: 20px;
}
</style>