add result panel.
This commit is contained in:
7
components.d.ts
vendored
7
components.d.ts
vendored
@@ -24,6 +24,7 @@ declare module '@vue/runtime-core' {
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
@@ -38,10 +39,14 @@ declare module '@vue/runtime-core' {
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
IEpBell: typeof import('~icons/ep/bell')['default']
|
||||
'IEpCaret-': typeof import('~icons/ep/caret-')['default']
|
||||
IEpCaretRight: typeof import('~icons/ep/caret-right')['default']
|
||||
IEpLoading: typeof import('~icons/ep/loading')['default']
|
||||
IEpSelect: typeof import('~icons/ep/select')['default']
|
||||
IEpSemiSelect: typeof import('~icons/ep/semi-select')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
UseNetwork: typeof import('@vueuse/components')['UseNetwork']
|
||||
UseNetwork: typeof import("@vueuse/components")["UseNetwork"]
|
||||
}
|
||||
}
|
||||
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"axios": "^1.2.2",
|
||||
"element-plus": "^2.2.28",
|
||||
"normalize.css": "^8.0.1",
|
||||
"party-js": "^2.2.0",
|
||||
"pinia": "^2.0.28",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
@@ -1618,6 +1619,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/party-js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/party-js/-/party-js-2.2.0.tgz",
|
||||
"integrity": "sha512-50hGuALCpvDTrQLPQ1fgUgxKIWAH28ShVkmeK/3zhO0YJyCqkhrZhQEkWPxDYLvbFJ7YAXyROmFEu35gKpZLtQ=="
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -3324,6 +3330,11 @@
|
||||
"p-limit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"party-js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/party-js/-/party-js-2.2.0.tgz",
|
||||
"integrity": "sha512-50hGuALCpvDTrQLPQ1fgUgxKIWAH28ShVkmeK/3zhO0YJyCqkhrZhQEkWPxDYLvbFJ7YAXyROmFEu35gKpZLtQ=="
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"axios": "^1.2.2",
|
||||
"element-plus": "^2.2.28",
|
||||
"normalize.css": "^8.0.1",
|
||||
"party-js": "^2.2.0",
|
||||
"pinia": "^2.0.28",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import zhCn from "element-plus/dist/locale/zh-cn.mjs"
|
||||
const locale = zhCn
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<el-config-provider :locale="zhCn" :button="{ autoInsertSpace: true }">
|
||||
<router-view></router-view>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
48
src/main.ts
48
src/main.ts
@@ -5,58 +5,12 @@ import "normalize.css"
|
||||
import loader from "@monaco-editor/loader"
|
||||
|
||||
import App from "./App.vue"
|
||||
import Home from "./oj/index.vue"
|
||||
import Problems from "./oj/problem/list.vue"
|
||||
|
||||
import storage from "./utils/storage"
|
||||
import routes from "./routes"
|
||||
import { STORAGE_KEY } from "./utils/constants"
|
||||
import { useLoginStore } from "./shared/stores/login"
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
component: Home,
|
||||
children: [
|
||||
{ path: "", component: Problems },
|
||||
{
|
||||
path: "problem/:problemID",
|
||||
component: () => import("./oj/problem/detail.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "status",
|
||||
component: () => import("./oj/status/list.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "status/:statusID",
|
||||
component: () => import("./oj/status/detail.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "contest",
|
||||
component: () => import("./oj/contest/list.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "contest/:contestID",
|
||||
component: () => import("./oj/contest/detail.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "contest/:contestID/problem/:problemID",
|
||||
component: () => import("./oj/problem/detail.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "rank",
|
||||
component: () => import("./oj/rank/list.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: "/admin", component: () => import("./admin/index.vue") },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getACRate } from "./../utils/functions"
|
||||
import { DIFFICULTY } from "./../utils/constants"
|
||||
import { Problem, SubmitCodePayload } from "./../utils/types"
|
||||
import { Problem, SubmitCodePayload, Submission } from "./../utils/types"
|
||||
import http from "./../utils/http"
|
||||
import { useAxios } from "@vueuse/integrations/useAxios"
|
||||
|
||||
@@ -60,11 +60,17 @@ export function getProblem(id: string) {
|
||||
}
|
||||
|
||||
export function getSubmission(id: string) {
|
||||
return http.get("submission", {
|
||||
return http.get<Submission>("submission", {
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export function submissionExists(problemID: number) {
|
||||
return http.get("submission_exists", {
|
||||
params: { problem_id: problemID },
|
||||
})
|
||||
}
|
||||
|
||||
export function submitCode(data: SubmitCodePayload) {
|
||||
return http.post("submission", data)
|
||||
}
|
||||
|
||||
16
src/oj/components/submission-result-tag.vue
Normal file
16
src/oj/components/submission-result-tag.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { JUDGE_STATUS } from "../../utils/constants"
|
||||
import { SUBMISSION_RESULT } from "../../utils/types"
|
||||
|
||||
const { result } = defineProps<{
|
||||
result: SUBMISSION_RESULT
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tag :type="JUDGE_STATUS[result]['type']" disable-transitions>
|
||||
{{ JUDGE_STATUS[result]["name"] }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>contest detail</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
277
src/oj/problem/components/editor-exec.vue
Normal file
277
src/oj/problem/components/editor-exec.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<script setup lang="ts">
|
||||
import { useTimeout, useTimeoutFn, useToggle } from "@vueuse/core"
|
||||
import { TabsPaneContext } from "element-plus"
|
||||
import party from "party-js"
|
||||
import { computed, onMounted, ref, watch } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import {
|
||||
SOURCES,
|
||||
JUDGE_STATUS,
|
||||
SubmissionStatus,
|
||||
} from "../../../utils/constants"
|
||||
import {
|
||||
submissionMemoryFormat,
|
||||
submissionTimeFormat,
|
||||
} from "../../../utils/functions"
|
||||
import {
|
||||
LANGUAGE,
|
||||
Problem,
|
||||
Submission,
|
||||
SubmitCodePayload,
|
||||
} from "../../../utils/types"
|
||||
import { getSubmission, submissionExists, submitCode } from "../../api"
|
||||
|
||||
import SubmissionResultTag from "../../components/submission-result-tag.vue"
|
||||
|
||||
interface Props {
|
||||
state: {
|
||||
language: LANGUAGE
|
||||
code: string
|
||||
}
|
||||
problem: Problem
|
||||
}
|
||||
|
||||
enum Tab {
|
||||
testcase = "testcase",
|
||||
result = "result",
|
||||
}
|
||||
|
||||
const { state, problem } = defineProps<Props>()
|
||||
|
||||
const route = useRoute()
|
||||
const contestID = <string>route.params.contestID || ""
|
||||
|
||||
const submission = ref<Submission | null>(null)
|
||||
const submissionId = ref("")
|
||||
const tab = ref(Tab.testcase)
|
||||
|
||||
const [submitted] = useToggle()
|
||||
const [tried] = useToggle()
|
||||
|
||||
const { start: submitPending, isPending } = useTimeout(5000, {
|
||||
controls: true,
|
||||
immediate: false,
|
||||
})
|
||||
|
||||
const { start: fetchSubmission } = useTimeoutFn(
|
||||
async () => {
|
||||
const res = await getSubmission(submissionId.value)
|
||||
submission.value = res.data
|
||||
const result = submission.value.result
|
||||
if (
|
||||
result === SubmissionStatus.judging ||
|
||||
result === SubmissionStatus.pending
|
||||
) {
|
||||
fetchSubmission()
|
||||
} else {
|
||||
submitted.value = false
|
||||
}
|
||||
},
|
||||
2000,
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
checkIfTried()
|
||||
})
|
||||
|
||||
async function checkIfTried() {
|
||||
const res = await submissionExists(problem.id)
|
||||
tried.value = res.data
|
||||
}
|
||||
|
||||
const judging = computed(
|
||||
() =>
|
||||
!!(submission.value && submission.value.result === SubmissionStatus.judging)
|
||||
)
|
||||
|
||||
const pending = computed(
|
||||
() =>
|
||||
!!(submission.value && submission.value.result === SubmissionStatus.pending)
|
||||
)
|
||||
|
||||
const submitting = computed(
|
||||
() =>
|
||||
!!(
|
||||
submission.value &&
|
||||
submission.value.result === SubmissionStatus.submitting
|
||||
)
|
||||
)
|
||||
|
||||
const submitDisabled = computed(() => {
|
||||
const code = state.code
|
||||
if (
|
||||
code.trim() === "" ||
|
||||
code === problem.template[state.language] ||
|
||||
code === SOURCES[state.language]
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (judging.value || pending.value || submitting.value) {
|
||||
return true
|
||||
}
|
||||
if (submitted.value) {
|
||||
return true
|
||||
}
|
||||
if (isPending.value) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const submitLabel = computed(() => {
|
||||
if (submitting.value) {
|
||||
return "正在提交"
|
||||
}
|
||||
if (judging.value || pending.value) {
|
||||
return "正在评分"
|
||||
}
|
||||
if (isPending.value) {
|
||||
return "运行结果"
|
||||
}
|
||||
return "点击提交"
|
||||
})
|
||||
|
||||
const msg = computed(() => {
|
||||
if (
|
||||
submission.value &&
|
||||
submission.value.statistic_info &&
|
||||
submission.value.statistic_info.err_info
|
||||
) {
|
||||
return submission.value.statistic_info.err_info
|
||||
}
|
||||
const result = submission.value && submission.value.result
|
||||
if (
|
||||
result === SubmissionStatus.compile_error ||
|
||||
result === SubmissionStatus.runtime_error
|
||||
) {
|
||||
return "请仔细检查,看看代码格式是不是写错了!"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
})
|
||||
|
||||
const infoTable = computed(() => {
|
||||
if (
|
||||
submission.value &&
|
||||
submission.value.result !== SubmissionStatus.accepted &&
|
||||
submission.value.result !== SubmissionStatus.compile_error &&
|
||||
submission.value.result !== SubmissionStatus.runtime_error &&
|
||||
submission.value.info &&
|
||||
submission.value.info.data &&
|
||||
submission.value.info.data.length
|
||||
) {
|
||||
return submission.value.info.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
const data: SubmitCodePayload = {
|
||||
problem_id: problem.id,
|
||||
language: state.language,
|
||||
code: state.code,
|
||||
}
|
||||
if (contestID) {
|
||||
data.contest_id = parseInt(contestID)
|
||||
}
|
||||
submission.value = { result: 9 } as Submission
|
||||
const res = await submitCode(data)
|
||||
submissionId.value = res.data.submission_id
|
||||
// 防止重复提交
|
||||
submitPending()
|
||||
submitted.value = true
|
||||
// 查询结果
|
||||
fetchSubmission()
|
||||
}
|
||||
|
||||
function onTab(pane: TabsPaneContext) {
|
||||
if (pane.paneName === Tab.result) {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => submission.value && submission.value.result,
|
||||
(result) => {
|
||||
if (result === SubmissionStatus.accepted) {
|
||||
party.confetti(document.body, {
|
||||
count: party.variation.range(200, 400),
|
||||
size: party.variation.skew(2, 0.3),
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tabs type="border-card" @tab-click="onTab" v-model="tab">
|
||||
<el-tab-pane label="测试用例" :name="Tab.testcase">
|
||||
<div class="panel"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :disabled="submitDisabled" :name="Tab.result">
|
||||
<template #label>
|
||||
<el-space>
|
||||
<el-icon>
|
||||
<i-ep-loading v-if="judging || pending || submitting" />
|
||||
<i-ep-bell v-else-if="isPending" />
|
||||
<i-ep-caret-right v-else />
|
||||
</el-icon>
|
||||
<span>{{ submitLabel }}</span>
|
||||
</el-space>
|
||||
</template>
|
||||
<div class="panel">
|
||||
<el-alert
|
||||
v-if="submission"
|
||||
:closable="false"
|
||||
:type="JUDGE_STATUS[submission.result]['type']"
|
||||
:title="JUDGE_STATUS[submission.result]['name']"
|
||||
>
|
||||
</el-alert>
|
||||
<el-scrollbar
|
||||
v-if="msg || infoTable.length"
|
||||
height="280"
|
||||
class="result"
|
||||
noresize
|
||||
>
|
||||
<div v-if="msg">{{ msg }}</div>
|
||||
<el-table v-if="infoTable.length" :data="infoTable" stripe>
|
||||
<el-table-column prop="test_case" label="测试用例" align="center" />
|
||||
<el-table-column label="测试状态" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<SubmissionResultTag
|
||||
v-if="scope.row"
|
||||
:result="scope.row.result"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="占用内存" align="center">
|
||||
<template #default="scope">
|
||||
{{ submissionMemoryFormat(scope.row.memory) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行耗时" align="center">
|
||||
<template #default="scope">
|
||||
{{ submissionTimeFormat(scope.row.real_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="signal" label="信号" align="center" />
|
||||
</el-table>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
white-space: pre;
|
||||
line-height: 1.2;
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import loader, { Monaco } from "@monaco-editor/loader"
|
||||
import { ref, onBeforeUnmount, onMounted, watch, reactive, computed } from "vue"
|
||||
import { ref, onBeforeUnmount, onMounted, watch, reactive } from "vue"
|
||||
import {
|
||||
LANGUAGE_LABEL,
|
||||
LANGUAGE_VALUE,
|
||||
SOURCES,
|
||||
} from "../../../utils/constants"
|
||||
import { isMobile } from "../../../utils/breakpoints"
|
||||
import { submitCode } from "../../api"
|
||||
import { LANGUAGE, Problem, SubmitCodePayload } from "../../../utils/types"
|
||||
import { Problem } from "../../../utils/types"
|
||||
import EditorExec from "./editor-exec.vue"
|
||||
|
||||
const { problem, contestID = "" } = defineProps<{
|
||||
contestID?: string
|
||||
problemID?: string
|
||||
problem: Problem
|
||||
}>()
|
||||
const { problem } = defineProps<{ problem: Problem }>()
|
||||
const state = reactive({
|
||||
values: ref({ ...SOURCES }),
|
||||
code: SOURCES[problem.languages[0] || "C"],
|
||||
language: problem.languages[0] || "C",
|
||||
isMobile,
|
||||
submissionId: "",
|
||||
})
|
||||
|
||||
const monacoEditorRef = ref()
|
||||
|
||||
let monaco: Monaco
|
||||
|
||||
function reset() {
|
||||
state.values[state.language] =
|
||||
problem.template[state.language] || SOURCES[state.language]
|
||||
if (monaco && monaco.editor) {
|
||||
monaco.editor.getModels()[0].setValue(state.values[state.language])
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(init)
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
monaco.editor.getModels().forEach((model) => model.dispose())
|
||||
@@ -52,13 +41,18 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
async function init() {
|
||||
state.values[state.language] =
|
||||
problem.template[state.language] || SOURCES[state.language]
|
||||
function reset() {
|
||||
state.code = problem.template[state.language] || SOURCES[state.language]
|
||||
if (monaco && monaco.editor) {
|
||||
monaco.editor.getModels()[0].setValue(state.code)
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
state.code = problem.template[state.language] || SOURCES[state.language]
|
||||
monaco = await loader.init()
|
||||
monaco.editor.create(monacoEditorRef.value, {
|
||||
value: state.values[state.language], // 编辑器初始显示文字
|
||||
value: state.code, // 编辑器初始显示文字
|
||||
language: LANGUAGE_VALUE[state.language],
|
||||
theme: "vs", // 官方自带三种主题vs, hc-black, or vs-dark
|
||||
minimap: {
|
||||
@@ -67,39 +61,17 @@ async function init() {
|
||||
lineNumbersMinChars: 3,
|
||||
automaticLayout: true, // 自适应布局
|
||||
tabSize: 4,
|
||||
fontSize: state.isMobile ? 16 : 24, // 字体大小
|
||||
fontSize: state.isMobile ? 20 : 24, // 字体大小
|
||||
scrollBeyondLastLine: false, // 取消代码后面一大段空白
|
||||
})
|
||||
monaco.editor.getModels()[0].onDidChangeContent(() => {
|
||||
state.values[state.language] = monaco.editor.getModels()[0].getValue()
|
||||
state.code = monaco.editor.getModels()[0].getValue()
|
||||
})
|
||||
}
|
||||
|
||||
const submitDisabled = computed(() => {
|
||||
const code = state.values[state.language]
|
||||
return (
|
||||
code.trim() === "" ||
|
||||
code === problem.template[state.language] ||
|
||||
code === SOURCES[state.language]
|
||||
)
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
const data: SubmitCodePayload = {
|
||||
problem_id: problem.id,
|
||||
language: state.language,
|
||||
code: state.values[state.language],
|
||||
}
|
||||
if (contestID) {
|
||||
data.contest_id = parseInt(contestID)
|
||||
}
|
||||
const res = await submitCode(data)
|
||||
state.submissionId = res.data.submission_id
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form :inline="true">
|
||||
<el-form inline>
|
||||
<el-form-item label="语言" label-width="60">
|
||||
<el-select v-model="state.language" class="language">
|
||||
<el-option
|
||||
@@ -115,22 +87,11 @@ async function submit() {
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div
|
||||
<section
|
||||
ref="monacoEditorRef"
|
||||
:class="isMobile ? 'editorMobile' : 'editor'"
|
||||
></div>
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="测试用例"> 1 </el-tab-pane>
|
||||
<el-tab-pane label="执行结果"> 2 </el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form class="actions">
|
||||
<el-form-item>
|
||||
<el-button>运行</el-button>
|
||||
<el-button type="primary" :disabled="submitDisabled" @click="submit">
|
||||
提交
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
></section>
|
||||
<EditorExec :state="state" :problem="problem" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -139,15 +100,11 @@ async function submit() {
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: 70%;
|
||||
/* 141px+400 */
|
||||
height: calc(100vh - 541px);
|
||||
}
|
||||
|
||||
.editorMobile {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@ const { data: problem, isFinished } = getProblem(problemID)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-row v-if="isFinished && problem">
|
||||
<el-row v-if="isFinished && problem" :gutter="20">
|
||||
<el-col :span="isDesktop ? 12 : 24">
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="题目描述">
|
||||
@@ -29,17 +29,13 @@ const { data: problem, isFinished } = getProblem(problemID)
|
||||
<el-tab-pane label="题目信息" lazy>
|
||||
<ProblemInfo :problem="problem" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="提交情况">3</el-tab-pane>
|
||||
<el-tab-pane label="提交列表">3</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-col>
|
||||
<el-col v-if="isDesktop" :span="12" class="editorWrapper">
|
||||
<el-col v-if="isDesktop" :span="12">
|
||||
<Editor :problem="problem" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.editorWrapper {
|
||||
height: calc(100vh - 171px);
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -100,7 +100,7 @@ onMounted(listProblems)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form :inline="true">
|
||||
<el-form inline>
|
||||
<el-form-item label="难度">
|
||||
<el-select v-model="query.difficulty">
|
||||
<el-option
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useToggle } from "@vueuse/core"
|
||||
import { defineStore } from "pinia"
|
||||
import { ref } from "vue"
|
||||
|
||||
export const useSignupStore = defineStore("signup", () => {
|
||||
const visible = ref(false)
|
||||
const [visible] = useToggle()
|
||||
|
||||
function show() {
|
||||
visible.value = true
|
||||
|
||||
49
src/routes.ts
Normal file
49
src/routes.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import Home from "./oj/index.vue"
|
||||
import Problems from "./oj/problem/list.vue"
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
component: Home,
|
||||
children: [
|
||||
{ path: "", component: Problems },
|
||||
{
|
||||
path: "problem/:problemID",
|
||||
component: () => import("./oj/problem/detail.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "status",
|
||||
component: () => import("./oj/status/list.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "status/:statusID",
|
||||
component: () => import("./oj/status/detail.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "contest",
|
||||
component: () => import("./oj/contest/list.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "contest/:contestID",
|
||||
component: () => import("./oj/contest/detail.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "contest/:contestID/problem/:problemID",
|
||||
component: () => import("./oj/problem/detail.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "rank",
|
||||
component: () => import("./oj/rank/list.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: "/admin", component: () => import("./admin/index.vue") },
|
||||
]
|
||||
|
||||
export default routes
|
||||
140
src/shared/split-panel/index.vue
Normal file
140
src/shared/split-panel/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<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"
|
||||
|
||||
const {
|
||||
minPercent = 10,
|
||||
defaultPercent = 50,
|
||||
split,
|
||||
className,
|
||||
} = defineProps<{
|
||||
minPercent?: number
|
||||
defaultPercent?: number
|
||||
split: "vertical" | "horizontal"
|
||||
className?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["resize"])
|
||||
|
||||
const active = ref(false)
|
||||
const hasMoved = ref(false)
|
||||
const percent = ref(defaultPercent)
|
||||
const type = ref(split === "vertical" ? "width" : "height")
|
||||
const resizeType = ref(split === "vertical" ? "left" : "top")
|
||||
|
||||
const userSelect = computed(() => (active.value ? "none" : "auto"))
|
||||
const cursor = computed(() =>
|
||||
active.value ? (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 (split === "vertical") {
|
||||
while (target) {
|
||||
offset += target.offsetLeft
|
||||
target = target.offsetParent
|
||||
}
|
||||
} else {
|
||||
while (target) {
|
||||
offset += target.offsetTop
|
||||
target = target.offsetParent
|
||||
}
|
||||
}
|
||||
const currentPage = split === "vertical" ? e.pageX : e.pageY
|
||||
const targetOffset =
|
||||
split === "vertical"
|
||||
? e.currentTarget.offsetWidth
|
||||
: e.currentTarget.offsetHeight
|
||||
const newPercent =
|
||||
Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100
|
||||
if (newPercent > minPercent && newPercent < 100 - 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>
|
||||
40
src/shared/split-panel/pane.vue
Normal file
40
src/shared/split-panel/pane.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { className, split } = defineProps<{
|
||||
split: "horizontal" | "vertical"
|
||||
className?: string
|
||||
}>()
|
||||
|
||||
const classes = $computed(() => [split, 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>
|
||||
43
src/shared/split-panel/resizer.vue
Normal file
43
src/shared/split-panel/resizer.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div :class="classes"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
|
||||
const { className, split } = defineProps<{
|
||||
split: "horizontal" | "vertical"
|
||||
className?: string
|
||||
}>()
|
||||
|
||||
const classes = computed(() =>
|
||||
["splitter-pane-resizer", split, 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,8 +1,8 @@
|
||||
import { useToggle } from "@vueuse/core"
|
||||
import { defineStore } from "pinia"
|
||||
import { ref } from "vue"
|
||||
|
||||
export const useLoginStore = defineStore("login", () => {
|
||||
const visible = ref(false)
|
||||
const [visible] = useToggle()
|
||||
|
||||
function show() {
|
||||
visible.value = true
|
||||
|
||||
@@ -1,72 +1,65 @@
|
||||
export enum SubmissionStatus {
|
||||
compile_error = -2,
|
||||
wrong_answer = -1,
|
||||
accepted = 0,
|
||||
time_limit_exceeded = 1 | 2,
|
||||
memory_limit_exceeded = 3,
|
||||
runtime_error = 4,
|
||||
system_error = 5,
|
||||
pending = 6,
|
||||
judging = 7,
|
||||
partial_accepted = 8,
|
||||
submitting = 9,
|
||||
}
|
||||
|
||||
export const JUDGE_STATUS = {
|
||||
"-2": {
|
||||
name: "Compile Error",
|
||||
short: "CE",
|
||||
color: "yellow",
|
||||
type: "warning",
|
||||
name: "编译失败",
|
||||
type: "error",
|
||||
},
|
||||
"-1": {
|
||||
name: "Wrong Answer",
|
||||
short: "WA",
|
||||
color: "red",
|
||||
name: "答案错误",
|
||||
type: "error",
|
||||
},
|
||||
"0": {
|
||||
name: "Accepted",
|
||||
short: "AC",
|
||||
color: "green",
|
||||
name: "答案正确",
|
||||
type: "success",
|
||||
},
|
||||
"1": {
|
||||
name: "Time Limit Exceeded",
|
||||
short: "TLE",
|
||||
color: "red",
|
||||
name: "运行超时",
|
||||
type: "error",
|
||||
},
|
||||
"2": {
|
||||
name: "Time Limit Exceeded",
|
||||
short: "TLE",
|
||||
color: "red",
|
||||
name: "运行超时",
|
||||
type: "error",
|
||||
},
|
||||
"3": {
|
||||
name: "Memory Limit Exceeded",
|
||||
short: "MLE",
|
||||
color: "red",
|
||||
name: "内存超限",
|
||||
type: "error",
|
||||
},
|
||||
"4": {
|
||||
name: "Runtime Error",
|
||||
short: "RE",
|
||||
color: "red",
|
||||
name: "运行时错误",
|
||||
type: "error",
|
||||
},
|
||||
"5": {
|
||||
name: "System Error",
|
||||
short: "SE",
|
||||
color: "red",
|
||||
name: "系统错误",
|
||||
type: "error",
|
||||
},
|
||||
"6": {
|
||||
name: "Pending",
|
||||
color: "yellow",
|
||||
name: "等待评分",
|
||||
type: "warning",
|
||||
},
|
||||
"7": {
|
||||
name: "Judging",
|
||||
color: "blue",
|
||||
name: "正在评分",
|
||||
type: "info",
|
||||
},
|
||||
"8": {
|
||||
name: "Partial Accepted",
|
||||
short: "PAC",
|
||||
color: "blue",
|
||||
type: "info",
|
||||
name: "部分正确",
|
||||
type: "warning",
|
||||
},
|
||||
"9": {
|
||||
name: "Submitting",
|
||||
color: "yellow",
|
||||
type: "warning",
|
||||
name: "正在提交",
|
||||
type: "info",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -38,3 +38,15 @@ export function parseTime(utc: Date, format = "YYYY年M月D日") {
|
||||
const time = useDateFormat(utc, format, { locales: "zh-CN" })
|
||||
return time.value
|
||||
}
|
||||
|
||||
export function submissionMemoryFormat(memory: string) {
|
||||
if (memory === undefined) return "--"
|
||||
// 1048576 = 1024 * 1024
|
||||
let t = parseInt(memory) / 1048576
|
||||
return String(t.toFixed(0)) + "MB"
|
||||
}
|
||||
|
||||
export function submissionTimeFormat(time: number) {
|
||||
if (time === undefined) return "--"
|
||||
return time + "ms"
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ const http = axios.create({
|
||||
xsrfCookieName: "csrftoken",
|
||||
})
|
||||
|
||||
// TODO
|
||||
http.interceptors.response.use(
|
||||
(res) => {
|
||||
if (res.data.error) {
|
||||
// 若后端返回为登录,则为session失效,应退出当前登录用户
|
||||
// // TODO: 若后端返回为登录,则为session失效,应退出当前登录用户
|
||||
if (res.data.data.startsWith("Please login")) {
|
||||
}
|
||||
return Promise.reject(res.data)
|
||||
|
||||
@@ -7,6 +7,8 @@ export type LANGUAGE =
|
||||
| "JavaScript"
|
||||
| "Golang"
|
||||
|
||||
export type SUBMISSION_RESULT = -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
|
||||
|
||||
export interface Problem {
|
||||
_id: string
|
||||
id: number
|
||||
@@ -56,3 +58,40 @@ export interface SubmitCodePayload {
|
||||
code: string
|
||||
contest_id?: number
|
||||
}
|
||||
|
||||
interface Info {
|
||||
err: string | null
|
||||
data: {
|
||||
error: number
|
||||
memory: number
|
||||
output: null
|
||||
result: SUBMISSION_RESULT
|
||||
signal: number
|
||||
cpu_time: number
|
||||
exit_code: number
|
||||
real_time: number
|
||||
test_case: string
|
||||
output_md5: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface Submission {
|
||||
id: string
|
||||
create_time: Date
|
||||
user_id: number
|
||||
username: string
|
||||
code: string
|
||||
result: SUBMISSION_RESULT
|
||||
info: Info
|
||||
language: string
|
||||
shared: boolean
|
||||
statistic_info: {
|
||||
score: number
|
||||
err_info: string
|
||||
}
|
||||
ip: string
|
||||
// TODO: 这里不知道是什么
|
||||
contest: null
|
||||
problem: number
|
||||
can_unshare: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user