naive-ui.
This commit is contained in:
19
README.md
19
README.md
@@ -1,18 +1 @@
|
|||||||
# Vue 3 + TypeScript + Vite
|
OJ next
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
|
||||||
|
|
||||||
## Recommended IDE Setup
|
|
||||||
|
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
|
||||||
|
|
||||||
## Type Support For `.vue` Imports in TS
|
|
||||||
|
|
||||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
|
||||||
|
|
||||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
|
||||||
|
|
||||||
1. Disable the built-in TypeScript Extension
|
|
||||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
|
||||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
|
||||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-Hans-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
|||||||
4454
package-lock.json
generated
Normal file
4454
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
|||||||
"axios": "^1.2.2",
|
"axios": "^1.2.2",
|
||||||
"copy-text-to-clipboard": "^3.0.1",
|
"copy-text-to-clipboard": "^3.0.1",
|
||||||
"element-plus": "^2.2.28",
|
"element-plus": "^2.2.28",
|
||||||
|
"naive-ui": "^2.34.3",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"party-js": "^2.2.0",
|
"party-js": "^2.2.0",
|
||||||
"pinia": "^2.0.28",
|
"pinia": "^2.0.28",
|
||||||
|
|||||||
1640
pnpm-lock.yaml
generated
1640
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import zhCn from "element-plus/dist/locale/zh-cn.mjs"
|
import { zhCN, dateZhCN, darkTheme } from "naive-ui"
|
||||||
|
import { isDark } from "./shared/composables/dark"
|
||||||
|
|
||||||
|
const theme = computed(() => (isDark.value ? darkTheme : null))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-config-provider :locale="zhCn">
|
<n-config-provider :theme="theme" :locale="zhCN" :date-locale="dateZhCN">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</el-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
4
src/auto-imports.d.ts
vendored
4
src/auto-imports.d.ts
vendored
@@ -149,6 +149,7 @@ declare global {
|
|||||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||||
|
const useDialog: typeof import('naive-ui')['useDialog']
|
||||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||||
@@ -181,6 +182,7 @@ declare global {
|
|||||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||||
const useLink: typeof import('vue-router')['useLink']
|
const useLink: typeof import('vue-router')['useLink']
|
||||||
|
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||||
@@ -188,6 +190,7 @@ declare global {
|
|||||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||||
|
const useMessage: typeof import('naive-ui')['useMessage']
|
||||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||||
@@ -195,6 +198,7 @@ declare global {
|
|||||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||||
|
const useNotification: typeof import('naive-ui')['useNotification']
|
||||||
const useNow: typeof import('@vueuse/core')['useNow']
|
const useNow: typeof import('@vueuse/core')['useNow']
|
||||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||||
|
|||||||
22
src/components.d.ts
vendored
22
src/components.d.ts
vendored
@@ -45,6 +45,28 @@ declare module '@vue/runtime-core' {
|
|||||||
IEpLoading: typeof import('~icons/ep/loading')['default']
|
IEpLoading: typeof import('~icons/ep/loading')['default']
|
||||||
IEpSelect: typeof import('~icons/ep/select')['default']
|
IEpSelect: typeof import('~icons/ep/select')['default']
|
||||||
IEpSemiSelect: typeof import('~icons/ep/semi-select')['default']
|
IEpSemiSelect: typeof import('~icons/ep/semi-select')['default']
|
||||||
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
|
NDropdownItem: typeof import('naive-ui')['NDropdownItem']
|
||||||
|
NDropdownMenu: typeof import('naive-ui')['NDropdownMenu']
|
||||||
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
|
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||||
|
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||||
|
NMenu: typeof import('naive-ui')['NMenu']
|
||||||
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
|
NPagination: typeof import('naive-ui')['NPagination']
|
||||||
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router"
|
import { createRouter, createWebHistory } from "vue-router"
|
||||||
import { createPinia } from "pinia"
|
import { createPinia } from "pinia"
|
||||||
import "normalize.css"
|
import "normalize.css"
|
||||||
import "element-plus/theme-chalk/dark/css-vars.css"
|
|
||||||
import loader from "@monaco-editor/loader"
|
import loader from "@monaco-editor/loader"
|
||||||
|
|
||||||
import storage from "utils/storage"
|
import storage from "utils/storage"
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ defineProps<Props>()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-tag :type="JUDGE_STATUS[result]['type']" disable-transitions>
|
<n-tag :type="JUDGE_STATUS[result]['type']">
|
||||||
{{ JUDGE_STATUS[result]["name"] }}
|
{{ JUDGE_STATUS[result]["name"] }}
|
||||||
</el-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ defineExpose({ submit })
|
|||||||
<el-alert
|
<el-alert
|
||||||
v-if="submission"
|
v-if="submission"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:type="JUDGE_STATUS[submission.result]['alertType']"
|
:type="JUDGE_STATUS[submission.result]['type']"
|
||||||
:title="JUDGE_STATUS[submission.result]['name']"
|
:title="JUDGE_STATUS[submission.result]['name']"
|
||||||
>
|
>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|||||||
@@ -1,38 +1,65 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { c, DataTableColumn, SelectOption } from "naive-ui"
|
||||||
|
import { NButton } from "naive-ui"
|
||||||
import Pagination from "~/shared/Pagination/index.vue"
|
import Pagination from "~/shared/Pagination/index.vue"
|
||||||
|
import SubmissionResultTag from "oj/components/SubmissionResultTag.vue"
|
||||||
import {
|
import {
|
||||||
submissionMemoryFormat,
|
submissionMemoryFormat,
|
||||||
submissionTimeFormat,
|
submissionTimeFormat,
|
||||||
parseTime,
|
parseTime,
|
||||||
filterEmptyValue,
|
filterEmptyValue,
|
||||||
} from "utils/functions"
|
} from "utils/functions"
|
||||||
|
import { Submission } from "utils/types"
|
||||||
import { getSubmissions } from "oj/api"
|
import { getSubmissions } from "oj/api"
|
||||||
import { isDesktop } from "~/shared/composables/breakpoints"
|
import { isDesktop } from "~/shared/composables/breakpoints"
|
||||||
import SubmissionResultTag from "oj/components/SubmissionResultTag.vue"
|
|
||||||
|
interface Query {
|
||||||
|
username: string
|
||||||
|
result: string
|
||||||
|
limit: number
|
||||||
|
page: number
|
||||||
|
myself: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const problemID = <string>route.query.problem ?? ""
|
const problemID = <string>route.query.problem ?? ""
|
||||||
const contestID = <string>route.query.contest ?? ""
|
const contestID = <string>route.query.contest ?? ""
|
||||||
|
|
||||||
const submissions = ref([])
|
const submissions = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const query = reactive({
|
const query = reactive<Query>({
|
||||||
|
result: <string>route.query.result ?? "",
|
||||||
page: parseInt(<string>route.query.page) || 1,
|
page: parseInt(<string>route.query.page) || 1,
|
||||||
limit: parseInt(<string>route.query.limit) || 10,
|
limit: parseInt(<string>route.query.limit) || 10,
|
||||||
username: <string>route.query.username ?? "",
|
username: <string>route.query.username ?? "",
|
||||||
myself: <"1" | "0">route.query.myself,
|
myself: route.query.myself === "1",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const options: SelectOption[] = [
|
||||||
|
{ label: "全部", value: "" },
|
||||||
|
{ label: "编译失败", value: "-2" },
|
||||||
|
{ label: "答案错误", value: "-1" },
|
||||||
|
{ label: "答案正确", value: "0" },
|
||||||
|
{ label: "运行超时", value: "1" },
|
||||||
|
{ label: "内存超限", value: "3" },
|
||||||
|
{ label: "运行时错误", value: "4" },
|
||||||
|
{ label: "系统错误", value: "5" },
|
||||||
|
{ label: "部分正确", value: "8" },
|
||||||
|
]
|
||||||
|
|
||||||
async function listSubmissions() {
|
async function listSubmissions() {
|
||||||
|
query.result = <string>route.query.result ?? ""
|
||||||
query.page = parseInt(<string>route.query.page) || 1
|
query.page = parseInt(<string>route.query.page) || 1
|
||||||
query.limit = parseInt(<string>route.query.limit) || 10
|
query.limit = parseInt(<string>route.query.limit) || 10
|
||||||
query.username = <string>route.query.username ?? ""
|
query.username = <string>route.query.username ?? ""
|
||||||
query.myself = <"1" | "0">route.query.myself
|
query.myself = route.query.myself === "1"
|
||||||
|
|
||||||
if (query.page < 1) query.page = 1
|
if (query.page < 1) query.page = 1
|
||||||
const offset = query.limit * (query.page - 1)
|
const offset = query.limit * (query.page - 1)
|
||||||
const res = await getSubmissions({
|
const res = await getSubmissions({
|
||||||
...query,
|
...query,
|
||||||
|
myself: query.myself ? "1" : "0",
|
||||||
offset,
|
offset,
|
||||||
problem_id: problemID,
|
problem_id: problemID,
|
||||||
contest_id: contestID,
|
contest_id: contestID,
|
||||||
@@ -44,16 +71,30 @@ async function listSubmissions() {
|
|||||||
onMounted(listSubmissions)
|
onMounted(listSubmissions)
|
||||||
|
|
||||||
function routerPush() {
|
function routerPush() {
|
||||||
|
const newQuery = {
|
||||||
|
...query,
|
||||||
|
myself: query.myself ? "1" : "0",
|
||||||
|
}
|
||||||
router.push({
|
router.push({
|
||||||
path: route.path,
|
path: route.path,
|
||||||
query: filterEmptyValue(query),
|
query: filterEmptyValue(newQuery),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function search(value: string) {
|
||||||
|
query.username = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
query.username = ""
|
||||||
|
query.myself = false
|
||||||
|
query.result = ""
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => query.page, routerPush)
|
watch(() => query.page, routerPush)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [query.limit, query.myself, query.username],
|
() => [query.limit, query.myself, query.username, query.result],
|
||||||
() => {
|
() => {
|
||||||
query.page = 1
|
query.page = 1
|
||||||
routerPush()
|
routerPush()
|
||||||
@@ -66,75 +107,88 @@ watch(
|
|||||||
if (newVal) listSubmissions()
|
if (newVal) listSubmissions()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const columns: DataTableColumn<Submission>[] = [
|
||||||
|
{
|
||||||
|
title: "提交时间",
|
||||||
|
key: "create_time",
|
||||||
|
width: 180,
|
||||||
|
render: (row) =>
|
||||||
|
parseTime(row.create_time, isDesktop ? "YYYY-M-D hh:mm:ss" : "M-D hh:mm"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "编号",
|
||||||
|
key: "id",
|
||||||
|
render: (row) =>
|
||||||
|
h(NButton, { text: true, type: "info" }, () => row.id.slice(0, 12)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
key: "status",
|
||||||
|
width: 120,
|
||||||
|
render: (row) => h(SubmissionResultTag, { result: row.result }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "题目",
|
||||||
|
key: "problem",
|
||||||
|
width: 100,
|
||||||
|
render: (row) =>
|
||||||
|
h(NButton, { text: true, type: "info" }, () => row.problem),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "执行耗时",
|
||||||
|
key: "time",
|
||||||
|
width: 100,
|
||||||
|
render: (row) => submissionTimeFormat(row.statistic_info.time_cost),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "占用内存",
|
||||||
|
key: "memory",
|
||||||
|
width: 100,
|
||||||
|
render: (row) => submissionMemoryFormat(row.statistic_info.memory_cost),
|
||||||
|
},
|
||||||
|
{ title: "语言", key: "language", width: 100 },
|
||||||
|
{
|
||||||
|
title: "提交者",
|
||||||
|
key: "username",
|
||||||
|
minWidth: 120,
|
||||||
|
render: (row) =>
|
||||||
|
h(NButton, { text: true, type: "info" }, () => row.username),
|
||||||
|
},
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<el-form inline>
|
<n-form :inline="isDesktop" label-placement="left">
|
||||||
<el-form-item label="提交状态">
|
<n-form-item label="提交状态">
|
||||||
<el-select></el-select>
|
<n-select
|
||||||
</el-form-item>
|
class="select"
|
||||||
<el-form-item label="查看自己">
|
v-model:value="query.result"
|
||||||
<el-switch></el-switch>
|
:options="options"
|
||||||
</el-form-item>
|
/>
|
||||||
<el-form-item label="搜索提交者">
|
</n-form-item>
|
||||||
<el-input></el-input>
|
<n-form-item label="只看自己">
|
||||||
</el-form-item>
|
<n-switch v-model:value="query.myself" />
|
||||||
</el-form>
|
</n-form-item>
|
||||||
<el-table :data="submissions" stripe>
|
<n-form-item label="搜索提交者">
|
||||||
<el-table-column
|
<n-input @change="search" clearable placeholder="输入后回车" />
|
||||||
label="提交时间"
|
</n-form-item>
|
||||||
prop="create_time"
|
<n-form-item>
|
||||||
:width="isDesktop ? 200 : 120"
|
<n-button @click="clear">重置</n-button>
|
||||||
>
|
</n-form-item>
|
||||||
<template #default="scope">
|
</n-form>
|
||||||
{{
|
<n-data-table striped size="small" :columns="columns" :data="submissions" />
|
||||||
parseTime(
|
|
||||||
scope.row.create_time,
|
|
||||||
isDesktop ? "YYYY-M-D hh:mm:ss" : "M-D hh:mm"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="编号" min-width="140">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-link type="primary">{{ scope.row.id.slice(0, 12) }}</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="状态" prop="result">
|
|
||||||
<template #default="scope">
|
|
||||||
<SubmissionResultTag :result="scope.row.result" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="题目" width="90">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-link
|
|
||||||
type="primary"
|
|
||||||
@click="$router.push(`/problem/${scope.row.problem}`)"
|
|
||||||
>
|
|
||||||
{{ scope.row.problem }}
|
|
||||||
</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column v-if="isDesktop" label="执行耗时" width="100">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ submissionTimeFormat(scope.row.statistic_info.time_cost) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column v-if="isDesktop" label="占用内存" width="100">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ submissionMemoryFormat(scope.row.statistic_info.memory_cost) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="语言" prop="language" width="100"></el-table-column>
|
|
||||||
<el-table-column label="提交者" min-width="120">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-link type="primary">{{ scope.row.username }}</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<Pagination
|
<Pagination
|
||||||
:total="total"
|
:total="total"
|
||||||
v-model:limit="query.limit"
|
v-model:limit="query.limit"
|
||||||
v-model:page="query.page"
|
v-model:page="query.page"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.select {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { logout } from "../api"
|
|||||||
import { useUserStore } from "../store/user"
|
import { useUserStore } from "../store/user"
|
||||||
import { isDark, toggleDark } from "~/shared/composables/dark"
|
import { isDark, toggleDark } from "~/shared/composables/dark"
|
||||||
import { toggleLogin, toggleSignup } from "~/shared/composables/modal"
|
import { toggleLogin, toggleSignup } from "~/shared/composables/modal"
|
||||||
import { isDesktop } from "../composables/breakpoints"
|
import type {
|
||||||
|
MenuOption,
|
||||||
|
DropdownOption,
|
||||||
|
DropdownDividerOption,
|
||||||
|
} from "naive-ui"
|
||||||
|
import { RouterLink } from "vue-router"
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -15,8 +20,8 @@ async function handleLogout() {
|
|||||||
router.replace("/")
|
router.replace("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDropdown(command: string) {
|
function handleDropdown(key: string) {
|
||||||
switch (command) {
|
switch (key) {
|
||||||
case "logout":
|
case "logout":
|
||||||
handleLogout()
|
handleLogout()
|
||||||
break
|
break
|
||||||
@@ -24,57 +29,66 @@ function handleDropdown(command: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(userStore.getMyProfile)
|
onMounted(userStore.getMyProfile)
|
||||||
|
|
||||||
|
const menus: MenuOption[] = [
|
||||||
|
{
|
||||||
|
label: () =>
|
||||||
|
h(RouterLink, { to: "/learn#step-1" }, { default: () => "自学" }),
|
||||||
|
key: "learn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(RouterLink, { to: "/" }, { default: () => "题库" }),
|
||||||
|
key: "problem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(RouterLink, { to: "/contest" }, { default: () => "比赛" }),
|
||||||
|
key: "contest",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(RouterLink, { to: "/status" }, { default: () => "提交" }),
|
||||||
|
key: "status",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(RouterLink, { to: "/rank" }, { default: () => "排名" }),
|
||||||
|
key: "rank",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const options = computed<Array<DropdownOption | DropdownDividerOption>>(() => [
|
||||||
|
{ label: "我的主页", key: "home" },
|
||||||
|
{ label: "我的提交", key: "status" },
|
||||||
|
{ label: "我的设置", key: "setting" },
|
||||||
|
{ label: "后台管理", key: "admin", show: userStore.isAdminRole },
|
||||||
|
{ type: "divider" },
|
||||||
|
{ label: "退出", key: "logout" },
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-menu
|
<n-space justify="space-between" align="center">
|
||||||
v-if="isDesktop"
|
<n-menu mode="horizontal" :options="menus" default-value="problem"></n-menu>
|
||||||
router
|
<n-space>
|
||||||
mode="horizontal"
|
<n-button circle @click="toggleDark()">
|
||||||
:default-active="$route.path"
|
<template #icon>
|
||||||
>
|
<n-icon v-if="isDark"><Sunny /></n-icon>
|
||||||
<el-menu-item index="/learn#step-1">自学</el-menu-item>
|
<n-icon v-else><Moon /></n-icon>
|
||||||
<el-menu-item index="/">题库</el-menu-item>
|
|
||||||
<el-menu-item index="/contest">比赛</el-menu-item>
|
|
||||||
<el-menu-item index="/status">提交</el-menu-item>
|
|
||||||
<el-menu-item index="/rank">排名</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
<el-space v-if="isDesktop" class="actions">
|
|
||||||
<el-button
|
|
||||||
circle
|
|
||||||
:icon="isDark ? Sunny : Moon"
|
|
||||||
@click="toggleDark()"
|
|
||||||
></el-button>
|
|
||||||
<div v-if="userStore.isFinished && !userStore.isAuthed">
|
|
||||||
<el-button @click="toggleLogin(true)">登录</el-button>
|
|
||||||
<el-button @click="toggleSignup(true)">注册</el-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="userStore.isFinished && userStore.isAuthed">
|
|
||||||
<el-dropdown @command="handleDropdown">
|
|
||||||
<el-button>{{ userStore.user.username }}</el-button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item>我的主页</el-dropdown-item>
|
|
||||||
<el-dropdown-item>我的提交</el-dropdown-item>
|
|
||||||
<el-dropdown-item>我的设置</el-dropdown-item>
|
|
||||||
<el-dropdown-item v-if="userStore.isAdminRole">
|
|
||||||
后台管理
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item divided command="logout">退出</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</n-button>
|
||||||
</div>
|
<div v-if="userStore.isFinished">
|
||||||
</el-space>
|
<n-dropdown
|
||||||
|
v-if="userStore.isAuthed"
|
||||||
|
:options="options"
|
||||||
|
@select="handleDropdown"
|
||||||
|
>
|
||||||
|
<n-button>{{ userStore.user.username }}</n-button>
|
||||||
|
</n-dropdown>
|
||||||
|
<n-space v-else>
|
||||||
|
<n-button @click="toggleLogin(true)">登录</n-button>
|
||||||
|
<n-button @click="toggleSignup(true)">注册</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.el-menu {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: solid 1px var(--el-menu-border-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FormInstance } from "element-plus"
|
|
||||||
import { login } from "../api"
|
import { login } from "../api"
|
||||||
import { loginModal, toggleLogin, toggleSignup } from "../composables/modal"
|
import { loginModal, toggleLogin, toggleSignup } from "../composables/modal"
|
||||||
import { useUserStore } from "../store/user"
|
import { useUserStore } from "../store/user"
|
||||||
|
import type { FormRules } from "naive-ui"
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const loginRef = ref<FormInstance>()
|
const loginRef = ref()
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
})
|
})
|
||||||
const rules = reactive({
|
const rules: FormRules = {
|
||||||
username: [{ required: true, message: "用户名必填", trigger: "blur" }],
|
username: [{ required: true, message: "用户名必填", trigger: "blur" }],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: "密码必填", trigger: "blur" },
|
{ required: true, message: "密码必填", trigger: "blur" },
|
||||||
{ min: 6, max: 20, message: "长度在6到20位之间", trigger: "change" },
|
{ min: 6, max: 20, message: "长度在6到20位之间", trigger: "input" },
|
||||||
],
|
],
|
||||||
})
|
}
|
||||||
const { isLoading, error, execute } = login(form)
|
const { isLoading, error, execute } = login(form)
|
||||||
const msg = computed(() => error.value && "用户名或密码不正确")
|
const msg = computed(() => error.value && "用户名或密码不正确")
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!loginRef.value) return
|
loginRef.value?.validate(async (errors: FormRules | undefined) => {
|
||||||
const valid = await loginRef.value.validate()
|
if (!errors) {
|
||||||
if (valid) {
|
await execute()
|
||||||
await execute()
|
if (!error.value) {
|
||||||
if (!error.value) {
|
toggleLogin(false)
|
||||||
toggleLogin(false)
|
userStore.getMyProfile()
|
||||||
userStore.getMyProfile()
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function goSignup() {
|
function goSignup() {
|
||||||
@@ -39,41 +39,43 @@ function goSignup() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<n-modal
|
||||||
style="max-width: 400px"
|
:mask-closable="false"
|
||||||
:close-on-click-modal="false"
|
v-model:show="loginModal"
|
||||||
:close-on-press-escape="false"
|
preset="card"
|
||||||
v-model="loginModal"
|
|
||||||
title="登录"
|
title="登录"
|
||||||
|
style="width: 400px"
|
||||||
|
:auto-focus="false"
|
||||||
>
|
>
|
||||||
<el-form
|
<n-form ref="loginRef" :model="form" :rules="rules" show-require-mark>
|
||||||
ref="loginRef"
|
<n-form-item label="用户名" path="username">
|
||||||
:model="form"
|
<n-input
|
||||||
:rules="rules"
|
v-model:value="form.username"
|
||||||
label-position="right"
|
autofocus
|
||||||
label-width="70px"
|
clearable
|
||||||
>
|
name="username"
|
||||||
<el-form-item label="用户名" required prop="username">
|
/>
|
||||||
<el-input v-model="form.username" name="username"></el-input>
|
</n-form-item>
|
||||||
</el-form-item>
|
<n-form-item label="密码" path="password">
|
||||||
<el-form-item label="密码" required prop="password">
|
<n-input
|
||||||
<el-input
|
v-model:value="form.password"
|
||||||
v-model="form.password"
|
clearable
|
||||||
type="password"
|
type="password"
|
||||||
show-password
|
|
||||||
@change="submit"
|
|
||||||
name="password"
|
name="password"
|
||||||
></el-input>
|
@change="submit"
|
||||||
</el-form-item>
|
/>
|
||||||
<el-form-item>
|
</n-form-item>
|
||||||
<el-button type="primary" :loading="isLoading" @click="submit">
|
<n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert>
|
||||||
登录
|
<n-form-item>
|
||||||
</el-button>
|
<n-space>
|
||||||
<el-button @click="goSignup">没有账号,立即注册</el-button>
|
<n-button type="primary" :loading="isLoading" @click="submit">
|
||||||
</el-form-item>
|
登录
|
||||||
<el-alert v-if="msg" :title="msg" show-icon type="error" />
|
</n-button>
|
||||||
</el-form>
|
<n-button @click="goSignup">没有账号,立即注册</n-button>
|
||||||
</el-dialog>
|
</n-space>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { isDesktop } from "~/shared/composables/breakpoints"
|
import { isDesktop } from "~/shared/composables/breakpoints"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
total: number
|
total: number
|
||||||
limit: number
|
limit: number
|
||||||
@@ -21,16 +22,15 @@ watch(page, () => emit("update:page", page))
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-pagination
|
<n-pagination
|
||||||
v-if="props.total"
|
v-if="props.total"
|
||||||
class="right margin"
|
class="right margin"
|
||||||
:layout="isDesktop ? 'prev,pager,next,sizes' : 'prev,next,sizes'"
|
:item-count="props.total"
|
||||||
background
|
v-model:page="page"
|
||||||
:total="props.total"
|
|
||||||
:page-sizes="[10, 20, 30]"
|
|
||||||
:pager-count="5"
|
|
||||||
v-model:page-size="limit"
|
v-model:page-size="limit"
|
||||||
v-model:current-page="page"
|
:page-sizes="[10, 20, 30]"
|
||||||
|
:page-slot="isDesktop ? 7 : 5"
|
||||||
|
show-size-picker
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,15 +1,79 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { signupModal } from "../composables/modal"
|
import type { FormRules } from "naive-ui"
|
||||||
|
import { signupModal, toggleLogin, toggleSignup } from "../composables/modal"
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
passwordAgain: "",
|
||||||
|
email: "",
|
||||||
|
})
|
||||||
|
const rules: FormRules = {}
|
||||||
|
|
||||||
|
const [isLoading] = useToggle()
|
||||||
|
const msg = ref("")
|
||||||
|
|
||||||
|
function goLogin() {
|
||||||
|
toggleLogin(true)
|
||||||
|
toggleSignup(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<n-modal
|
||||||
:close-on-click-modal="false"
|
:mask-closable="false"
|
||||||
:close-on-press-escape="false"
|
v-model:show="signupModal"
|
||||||
v-model="signupModal"
|
preset="card"
|
||||||
title="注册"
|
title="注册"
|
||||||
|
style="width: 400px"
|
||||||
|
:auto-focus="false"
|
||||||
>
|
>
|
||||||
</el-dialog>
|
<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="username"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="邮箱" path="email">
|
||||||
|
<n-input
|
||||||
|
v-model:value="form.email"
|
||||||
|
clearable
|
||||||
|
name="email"
|
||||||
|
@change="submit"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="密码" path="password">
|
||||||
|
<n-input
|
||||||
|
v-model:value="form.password"
|
||||||
|
clearable
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="确认密码" path="password">
|
||||||
|
<n-input
|
||||||
|
v-model:value="form.passwordAgain"
|
||||||
|
clearable
|
||||||
|
type="password"
|
||||||
|
name="passwordAgain"
|
||||||
|
/>
|
||||||
|
</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>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :class="classes">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from "vue"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
split: "horizontal" | "vertical"
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
split: "horizontal",
|
|
||||||
className: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const classes = computed(() => [props.split, props.className].join(" "))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.splitter-pane.vertical.splitter-paneL {
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
height: 100%;
|
|
||||||
padding-right: 3px;
|
|
||||||
}
|
|
||||||
.splitter-pane.vertical.splitter-paneR {
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
height: 100%;
|
|
||||||
padding-left: 3px;
|
|
||||||
}
|
|
||||||
.splitter-pane.horizontal.splitter-paneL {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.splitter-pane.horizontal.splitter-paneR {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
width: 100%;
|
|
||||||
padding-top: 3px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :class="classes"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from "vue"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
split: "horizontal" | "vertical"
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
split: "horizontal",
|
|
||||||
className: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const classes = computed(() =>
|
|
||||||
["splitter-pane-resizer", props.split, props.className].join(" ")
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.splitter-pane-resizer {
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #000;
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0.2;
|
|
||||||
z-index: 1;
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
.splitter-pane-resizer.horizontal {
|
|
||||||
height: 11px;
|
|
||||||
margin: -5px 0;
|
|
||||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
|
||||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
|
||||||
cursor: row-resize;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.splitter-pane-resizer.vertical {
|
|
||||||
width: 11px;
|
|
||||||
height: 100%;
|
|
||||||
margin-left: -5px;
|
|
||||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
|
||||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
|
||||||
cursor: col-resize;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
:style="{ cursor, userSelect }"
|
|
||||||
class="vue-splitter-container clearfix"
|
|
||||||
@mouseup="onMouseUp"
|
|
||||||
@mousemove="onMouseMove"
|
|
||||||
>
|
|
||||||
<Pane
|
|
||||||
class="splitter-pane splitter-paneL"
|
|
||||||
:split="split"
|
|
||||||
:style="{ [type]: percent + '%' }"
|
|
||||||
>
|
|
||||||
<slot name="panel"></slot>
|
|
||||||
</Pane>
|
|
||||||
|
|
||||||
<Resizer
|
|
||||||
:className="className"
|
|
||||||
:style="{ [resizeType]: percent + '%' }"
|
|
||||||
:split="split"
|
|
||||||
@mousedown.native="onMouseDown"
|
|
||||||
@click.native="onClick"
|
|
||||||
></Resizer>
|
|
||||||
|
|
||||||
<Pane
|
|
||||||
class="splitter-pane splitter-paneR"
|
|
||||||
:split="split"
|
|
||||||
:style="{ [type]: 100 - percent + '%' }"
|
|
||||||
>
|
|
||||||
<slot name="paner"></slot>
|
|
||||||
</Pane>
|
|
||||||
<div class="vue-splitter-container-mask" v-if="active"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Resizer from "./Resizer.vue"
|
|
||||||
import Pane from "./Pane.vue"
|
|
||||||
import { computed, ref } from "vue"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
minPercent?: number
|
|
||||||
defaultPercent?: number
|
|
||||||
split: "vertical" | "horizontal"
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
minPercent: 10,
|
|
||||||
defaultPercent: 50,
|
|
||||||
split: "horizontal",
|
|
||||||
className: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(["resize"])
|
|
||||||
|
|
||||||
const active = ref(false)
|
|
||||||
const hasMoved = ref(false)
|
|
||||||
const percent = ref(props.defaultPercent)
|
|
||||||
const type = ref(props.split === "vertical" ? "width" : "height")
|
|
||||||
const resizeType = ref(props.split === "vertical" ? "left" : "top")
|
|
||||||
|
|
||||||
const userSelect = computed(() => (active.value ? "none" : "auto"))
|
|
||||||
const cursor = computed(() =>
|
|
||||||
active.value ? (props.split === "vertical" ? "col-resize" : "row-resize") : ""
|
|
||||||
)
|
|
||||||
|
|
||||||
// watch(
|
|
||||||
// () => defaultPercent,
|
|
||||||
// (newValue) => {
|
|
||||||
// percent.value = newValue
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
|
|
||||||
function onClick() {
|
|
||||||
if (!hasMoved.value) {
|
|
||||||
percent.value = 50
|
|
||||||
emit("resize", percent.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onMouseDown() {
|
|
||||||
active.value = true
|
|
||||||
hasMoved.value = false
|
|
||||||
}
|
|
||||||
function onMouseUp() {
|
|
||||||
active.value = false
|
|
||||||
}
|
|
||||||
function onMouseMove(e: any) {
|
|
||||||
if (e.buttons === 0) {
|
|
||||||
active.value = false
|
|
||||||
}
|
|
||||||
if (active.value) {
|
|
||||||
let offset = 0
|
|
||||||
let target = e.currentTarget
|
|
||||||
if (props.split === "vertical") {
|
|
||||||
while (target) {
|
|
||||||
offset += target.offsetLeft
|
|
||||||
target = target.offsetParent
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
while (target) {
|
|
||||||
offset += target.offsetTop
|
|
||||||
target = target.offsetParent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const currentPage = props.split === "vertical" ? e.pageX : e.pageY
|
|
||||||
const targetOffset =
|
|
||||||
props.split === "vertical"
|
|
||||||
? e.currentTarget.offsetWidth
|
|
||||||
: e.currentTarget.offsetHeight
|
|
||||||
const newPercent =
|
|
||||||
Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100
|
|
||||||
if (newPercent > props.minPercent && newPercent < 100 - props.minPercent) {
|
|
||||||
percent.value = newPercent
|
|
||||||
}
|
|
||||||
emit("resize", newPercent)
|
|
||||||
hasMoved.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.clearfix:after {
|
|
||||||
visibility: hidden;
|
|
||||||
display: block;
|
|
||||||
font-size: 0;
|
|
||||||
content: " ";
|
|
||||||
clear: both;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
.vue-splitter-container {
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.vue-splitter-container-mask {
|
|
||||||
z-index: 9999;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export const isDark = useDark({ storageKey: "theme-appearance" })
|
export const isDark = useLocalStorage("theme-appearance", false)
|
||||||
export const toggleDark = useToggle(isDark)
|
export const toggleDark = useToggle(isDark)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container>
|
<n-layout>
|
||||||
<el-main>
|
<n-layout-content bordered>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</el-main>
|
</n-layout-content>
|
||||||
</el-container>
|
</n-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -5,20 +5,24 @@ import Header from "../Header/index.vue"
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container>
|
<n-layout>
|
||||||
<el-header class="header">
|
<n-layout-header bordered class="header">
|
||||||
<Header />
|
<Header />
|
||||||
</el-header>
|
</n-layout-header>
|
||||||
<el-main>
|
<n-layout-content class="content">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</el-main>
|
</n-layout-content>
|
||||||
<Login />
|
<Login />
|
||||||
<Signup />
|
<Signup />
|
||||||
</el-container>
|
</n-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
:style="{ cursor, userSelect }"
|
|
||||||
class="vue-splitter-container clearfix"
|
|
||||||
@mouseup="onMouseUp"
|
|
||||||
@mousemove="onMouseMove"
|
|
||||||
>
|
|
||||||
<Pane
|
|
||||||
class="splitter-pane splitter-paneL"
|
|
||||||
:split="split"
|
|
||||||
:style="{ [type]: percent + '%' }"
|
|
||||||
>
|
|
||||||
<slot name="panel"></slot>
|
|
||||||
</Pane>
|
|
||||||
|
|
||||||
<Resizer
|
|
||||||
:className="className"
|
|
||||||
:style="{ [resizeType]: percent + '%' }"
|
|
||||||
:split="split"
|
|
||||||
@mousedown.native="onMouseDown"
|
|
||||||
@click.native="onClick"
|
|
||||||
></Resizer>
|
|
||||||
|
|
||||||
<Pane
|
|
||||||
class="splitter-pane splitter-paneR"
|
|
||||||
:split="split"
|
|
||||||
:style="{ [type]: 100 - percent + '%' }"
|
|
||||||
>
|
|
||||||
<slot name="paner"></slot>
|
|
||||||
</Pane>
|
|
||||||
<div class="vue-splitter-container-mask" v-if="active"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Resizer from "./Resizer.vue"
|
|
||||||
import Pane from "./Pane.vue"
|
|
||||||
import { computed, ref } from "vue"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
minPercent?: number
|
|
||||||
defaultPercent?: number
|
|
||||||
split: "vertical" | "horizontal"
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
minPercent: 10,
|
|
||||||
defaultPercent: 50,
|
|
||||||
split: "horizontal",
|
|
||||||
className: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(["resize"])
|
|
||||||
|
|
||||||
const active = ref(false)
|
|
||||||
const hasMoved = ref(false)
|
|
||||||
const percent = ref(props.defaultPercent)
|
|
||||||
const type = ref(props.split === "vertical" ? "width" : "height")
|
|
||||||
const resizeType = ref(props.split === "vertical" ? "left" : "top")
|
|
||||||
|
|
||||||
const userSelect = computed(() => (active.value ? "none" : "auto"))
|
|
||||||
const cursor = computed(() =>
|
|
||||||
active.value ? (props.split === "vertical" ? "col-resize" : "row-resize") : ""
|
|
||||||
)
|
|
||||||
|
|
||||||
// watch(
|
|
||||||
// () => defaultPercent,
|
|
||||||
// (newValue) => {
|
|
||||||
// percent.value = newValue
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
|
|
||||||
function onClick() {
|
|
||||||
if (!hasMoved.value) {
|
|
||||||
percent.value = 50
|
|
||||||
emit("resize", percent.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onMouseDown() {
|
|
||||||
active.value = true
|
|
||||||
hasMoved.value = false
|
|
||||||
}
|
|
||||||
function onMouseUp() {
|
|
||||||
active.value = false
|
|
||||||
}
|
|
||||||
function onMouseMove(e: any) {
|
|
||||||
if (e.buttons === 0) {
|
|
||||||
active.value = false
|
|
||||||
}
|
|
||||||
if (active.value) {
|
|
||||||
let offset = 0
|
|
||||||
let target = e.currentTarget
|
|
||||||
if (props.split === "vertical") {
|
|
||||||
while (target) {
|
|
||||||
offset += target.offsetLeft
|
|
||||||
target = target.offsetParent
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
while (target) {
|
|
||||||
offset += target.offsetTop
|
|
||||||
target = target.offsetParent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const currentPage = props.split === "vertical" ? e.pageX : e.pageY
|
|
||||||
const targetOffset =
|
|
||||||
props.split === "vertical"
|
|
||||||
? e.currentTarget.offsetWidth
|
|
||||||
: e.currentTarget.offsetHeight
|
|
||||||
const newPercent =
|
|
||||||
Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100
|
|
||||||
if (newPercent > props.minPercent && newPercent < 100 - props.minPercent) {
|
|
||||||
percent.value = newPercent
|
|
||||||
}
|
|
||||||
emit("resize", newPercent)
|
|
||||||
hasMoved.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.clearfix:after {
|
|
||||||
visibility: hidden;
|
|
||||||
display: block;
|
|
||||||
font-size: 0;
|
|
||||||
content: " ";
|
|
||||||
clear: both;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
.vue-splitter-container {
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.vue-splitter-container-mask {
|
|
||||||
z-index: 9999;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
2
src/shims.d.ts
vendored
2
src/shims.d.ts
vendored
@@ -1,5 +1,3 @@
|
|||||||
declare module "element-plus/dist/locale/zh-cn.mjs"
|
|
||||||
|
|
||||||
declare module "*.md" {
|
declare module "*.md" {
|
||||||
import type { ComponentOptions } from "vue"
|
import type { ComponentOptions } from "vue"
|
||||||
const Component: ComponentOptions
|
const Component: ComponentOptions
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { SUBMISSION_RESULT } from "./types"
|
||||||
|
|
||||||
export enum SubmissionStatus {
|
export enum SubmissionStatus {
|
||||||
compile_error = -2,
|
compile_error = -2,
|
||||||
wrong_answer = -1,
|
wrong_answer = -1,
|
||||||
@@ -12,66 +14,59 @@ export enum SubmissionStatus {
|
|||||||
submitting = 9,
|
submitting = 9,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JUDGE_STATUS = {
|
export const JUDGE_STATUS: {
|
||||||
|
[key in SUBMISSION_RESULT]: {
|
||||||
|
name: string
|
||||||
|
type: "error" | "success" | "warning" | "info"
|
||||||
|
}
|
||||||
|
} = {
|
||||||
"-2": {
|
"-2": {
|
||||||
name: "编译失败",
|
name: "编译失败",
|
||||||
type: "danger",
|
type: "warning",
|
||||||
alertType: "error",
|
|
||||||
},
|
},
|
||||||
"-1": {
|
"-1": {
|
||||||
name: "答案错误",
|
name: "答案错误",
|
||||||
type: "danger",
|
type: "error",
|
||||||
alertType: "error",
|
|
||||||
},
|
},
|
||||||
"0": {
|
"0": {
|
||||||
name: "答案正确",
|
name: "答案正确",
|
||||||
type: "success",
|
type: "success",
|
||||||
alertType: "success",
|
|
||||||
},
|
},
|
||||||
"1": {
|
"1": {
|
||||||
name: "运行超时",
|
name: "运行超时",
|
||||||
type: "danger",
|
type: "error",
|
||||||
alertType: "error",
|
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
name: "运行超时",
|
name: "运行超时",
|
||||||
type: "danger",
|
type: "error",
|
||||||
alertType: "error",
|
|
||||||
},
|
},
|
||||||
"3": {
|
"3": {
|
||||||
name: "内存超限",
|
name: "内存超限",
|
||||||
type: "danger",
|
type: "error",
|
||||||
alertType: "error",
|
|
||||||
},
|
},
|
||||||
"4": {
|
"4": {
|
||||||
name: "运行时错误",
|
name: "运行时错误",
|
||||||
type: "danger",
|
type: "warning",
|
||||||
alertType: "error",
|
|
||||||
},
|
},
|
||||||
"5": {
|
"5": {
|
||||||
name: "系统错误",
|
name: "系统错误",
|
||||||
type: "danger",
|
type: "error",
|
||||||
alertType: "error",
|
|
||||||
},
|
},
|
||||||
"6": {
|
"6": {
|
||||||
name: "等待评分",
|
name: "等待评分",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
alertType: "warning",
|
|
||||||
},
|
},
|
||||||
"7": {
|
"7": {
|
||||||
name: "正在评分",
|
name: "正在评分",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
alertType: "warning",
|
|
||||||
},
|
},
|
||||||
"8": {
|
"8": {
|
||||||
name: "部分正确",
|
name: "部分正确",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
alertType: "warning",
|
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
name: "正在提交",
|
name: "正在提交",
|
||||||
type: "info",
|
type: "info",
|
||||||
alertType: "info",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ export function parseTime(utc: Date, format = "YYYY年M月D日") {
|
|||||||
return time.value
|
return time.value
|
||||||
}
|
}
|
||||||
|
|
||||||
export function submissionMemoryFormat(memory: string) {
|
export function submissionMemoryFormat(memory: number | string | undefined) {
|
||||||
if (memory === undefined) return "--"
|
if (memory === undefined) return "--"
|
||||||
// 1048576 = 1024 * 1024
|
// 1048576 = 1024 * 1024
|
||||||
let t = parseInt(memory) / 1048576
|
let t = parseInt(memory + "") / 1048576
|
||||||
return String(t.toFixed(0)) + "MB"
|
return String(t.toFixed(0)) + "MB"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function submissionTimeFormat(time: number) {
|
export function submissionTimeFormat(time: number | string | undefined) {
|
||||||
if (time === undefined) return "--"
|
if (time === undefined) return "--"
|
||||||
return time + "ms"
|
return time + "ms"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,8 +93,10 @@ export interface Submission {
|
|||||||
language: string
|
language: string
|
||||||
shared: boolean
|
shared: boolean
|
||||||
statistic_info: {
|
statistic_info: {
|
||||||
score: number
|
score?: number
|
||||||
err_info: string
|
err_info?: string
|
||||||
|
time_cost?: number
|
||||||
|
memory_cost?: number
|
||||||
}
|
}
|
||||||
ip: string
|
ip: string
|
||||||
// TODO: 这里不知道是什么
|
// TODO: 这里不知道是什么
|
||||||
@@ -105,7 +107,7 @@ export interface Submission {
|
|||||||
|
|
||||||
export interface SubmissionListPayload {
|
export interface SubmissionListPayload {
|
||||||
myself?: "1" | "0"
|
myself?: "1" | "0"
|
||||||
result?: SUBMISSION_RESULT
|
result?: string
|
||||||
username?: string
|
username?: string
|
||||||
contest_id?: string
|
contest_id?: string
|
||||||
problem_id?: string
|
problem_id?: string
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"oj/*": ["./src/oj/*"],
|
"oj/*": ["./src/oj/*"],
|
||||||
"admin/*": ["./src/admin/*"],
|
"admin/*": ["./src/admin/*"],
|
||||||
"learn/*": ["./src/learn/*"],
|
"learn/*": ["./src/learn/*"],
|
||||||
}
|
},
|
||||||
|
"types": ["naive-ui/volar"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import path from "path"
|
|||||||
import Vue from "@vitejs/plugin-vue"
|
import Vue from "@vitejs/plugin-vue"
|
||||||
import AutoImport from "unplugin-auto-import/vite"
|
import AutoImport from "unplugin-auto-import/vite"
|
||||||
import Components from "unplugin-vue-components/vite"
|
import Components from "unplugin-vue-components/vite"
|
||||||
import { ElementPlusResolver } from "unplugin-vue-components/resolvers"
|
import {
|
||||||
|
ElementPlusResolver,
|
||||||
|
NaiveUiResolver,
|
||||||
|
} from "unplugin-vue-components/resolvers"
|
||||||
import IconsResolver from "unplugin-icons/resolver"
|
import IconsResolver from "unplugin-icons/resolver"
|
||||||
import Icons from "unplugin-icons/vite"
|
import Icons from "unplugin-icons/vite"
|
||||||
import Markdown from "vite-plugin-vue-markdown"
|
import Markdown from "vite-plugin-vue-markdown"
|
||||||
@@ -28,12 +31,25 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
Vue({ include: [/\.vue$/, /\.md$/] }),
|
Vue({ include: [/\.vue$/, /\.md$/] }),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: ["vue", "vue-router", "@vueuse/core", "pinia"],
|
imports: [
|
||||||
resolvers: [ElementPlusResolver(), IconsResolver()],
|
"vue",
|
||||||
|
"vue-router",
|
||||||
|
"@vueuse/core",
|
||||||
|
"pinia",
|
||||||
|
{
|
||||||
|
"naive-ui": [
|
||||||
|
"useDialog",
|
||||||
|
"useMessage",
|
||||||
|
"useNotification",
|
||||||
|
"useLoadingBar",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
dts: "./src/auto-imports.d.ts",
|
dts: "./src/auto-imports.d.ts",
|
||||||
}),
|
}),
|
||||||
Components({
|
Components({
|
||||||
resolvers: [
|
resolvers: [
|
||||||
|
NaiveUiResolver(),
|
||||||
ElementPlusResolver(),
|
ElementPlusResolver(),
|
||||||
IconsResolver({ enabledCollections: ["ep"] }),
|
IconsResolver({ enabledCollections: ["ep"] }),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user