add result panel.

This commit is contained in:
2023-01-10 16:16:14 +08:00
parent a3456595a5
commit e3db109317
22 changed files with 714 additions and 172 deletions

7
components.d.ts vendored
View File

@@ -24,6 +24,7 @@ declare module '@vue/runtime-core' {
ElHeader: typeof import('element-plus/es')['ElHeader'] ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain'] ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -38,10 +39,14 @@ declare module '@vue/runtime-core' {
ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs'] ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag'] 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'] IEpSelect: typeof import('~icons/ep/select')['default']
IEpSemiSelect: typeof import('~icons/ep/semi-select')['default'] IEpSemiSelect: typeof import('~icons/ep/semi-select')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
UseNetwork: typeof import('@vueuse/components')['UseNetwork'] UseNetwork: typeof import("@vueuse/components")["UseNetwork"]
} }
} }

11
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"axios": "^1.2.2", "axios": "^1.2.2",
"element-plus": "^2.2.28", "element-plus": "^2.2.28",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"party-js": "^2.2.0",
"pinia": "^2.0.28", "pinia": "^2.0.28",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
@@ -1618,6 +1619,11 @@
"node": ">=10" "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": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
@@ -3324,6 +3330,11 @@
"p-limit": "^3.0.2" "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": { "path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",

View File

@@ -17,6 +17,7 @@
"axios": "^1.2.2", "axios": "^1.2.2",
"element-plus": "^2.2.28", "element-plus": "^2.2.28",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"party-js": "^2.2.0",
"pinia": "^2.0.28", "pinia": "^2.0.28",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"

View File

@@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import zhCn from "element-plus/dist/locale/zh-cn.mjs" import zhCn from "element-plus/dist/locale/zh-cn.mjs"
const locale = zhCn
</script> </script>
<template> <template>
<el-config-provider :locale="locale"> <el-config-provider :locale="zhCn" :button="{ autoInsertSpace: true }">
<router-view></router-view> <router-view></router-view>
</el-config-provider> </el-config-provider>
</template> </template>

View File

@@ -5,58 +5,12 @@ import "normalize.css"
import loader from "@monaco-editor/loader" import loader from "@monaco-editor/loader"
import App from "./App.vue" import App from "./App.vue"
import Home from "./oj/index.vue"
import Problems from "./oj/problem/list.vue"
import storage from "./utils/storage" import storage from "./utils/storage"
import routes from "./routes"
import { STORAGE_KEY } from "./utils/constants" import { STORAGE_KEY } from "./utils/constants"
import { useLoginStore } from "./shared/stores/login" 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({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes, routes,

View File

@@ -1,6 +1,6 @@
import { getACRate } from "./../utils/functions" import { getACRate } from "./../utils/functions"
import { DIFFICULTY } from "./../utils/constants" import { DIFFICULTY } from "./../utils/constants"
import { Problem, SubmitCodePayload } from "./../utils/types" import { Problem, SubmitCodePayload, Submission } from "./../utils/types"
import http from "./../utils/http" import http from "./../utils/http"
import { useAxios } from "@vueuse/integrations/useAxios" import { useAxios } from "@vueuse/integrations/useAxios"
@@ -60,11 +60,17 @@ export function getProblem(id: string) {
} }
export function getSubmission(id: string) { export function getSubmission(id: string) {
return http.get("submission", { return http.get<Submission>("submission", {
params: { id }, params: { id },
}) })
} }
export function submissionExists(problemID: number) {
return http.get("submission_exists", {
params: { problem_id: problemID },
})
}
export function submitCode(data: SubmitCodePayload) { export function submitCode(data: SubmitCodePayload) {
return http.post("submission", data) return http.post("submission", data)
} }

View 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>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>contest detail</template>
<style scoped></style>

View 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>

View File

@@ -1,39 +1,28 @@
<script lang="ts" setup> <script lang="ts" setup>
import loader, { Monaco } from "@monaco-editor/loader" 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 { import {
LANGUAGE_LABEL, LANGUAGE_LABEL,
LANGUAGE_VALUE, LANGUAGE_VALUE,
SOURCES, SOURCES,
} from "../../../utils/constants" } from "../../../utils/constants"
import { isMobile } from "../../../utils/breakpoints" import { isMobile } from "../../../utils/breakpoints"
import { submitCode } from "../../api" import { Problem } from "../../../utils/types"
import { LANGUAGE, Problem, SubmitCodePayload } from "../../../utils/types" import EditorExec from "./editor-exec.vue"
const { problem, contestID = "" } = defineProps<{ const { problem } = defineProps<{ problem: Problem }>()
contestID?: string
problemID?: string
problem: Problem
}>()
const state = reactive({ const state = reactive({
values: ref({ ...SOURCES }), code: SOURCES[problem.languages[0] || "C"],
language: problem.languages[0] || "C", language: problem.languages[0] || "C",
isMobile, isMobile,
submissionId: "",
}) })
const monacoEditorRef = ref() const monacoEditorRef = ref()
let monaco: Monaco let monaco: Monaco
function reset() { onMounted(() => {
state.values[state.language] = init()
problem.template[state.language] || SOURCES[state.language] })
if (monaco && monaco.editor) {
monaco.editor.getModels()[0].setValue(state.values[state.language])
}
}
onMounted(init)
onBeforeUnmount(() => { onBeforeUnmount(() => {
monaco.editor.getModels().forEach((model) => model.dispose()) monaco.editor.getModels().forEach((model) => model.dispose())
@@ -52,13 +41,18 @@ watch(
} }
) )
async function init() { function reset() {
state.values[state.language] = state.code = problem.template[state.language] || SOURCES[state.language]
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 = await loader.init()
monaco.editor.create(monacoEditorRef.value, { monaco.editor.create(monacoEditorRef.value, {
value: state.values[state.language], // 编辑器初始显示文字 value: state.code, // 编辑器初始显示文字
language: LANGUAGE_VALUE[state.language], language: LANGUAGE_VALUE[state.language],
theme: "vs", // 官方自带三种主题vs, hc-black, or vs-dark theme: "vs", // 官方自带三种主题vs, hc-black, or vs-dark
minimap: { minimap: {
@@ -67,39 +61,17 @@ async function init() {
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
automaticLayout: true, // 自适应布局 automaticLayout: true, // 自适应布局
tabSize: 4, tabSize: 4,
fontSize: state.isMobile ? 16 : 24, // 字体大小 fontSize: state.isMobile ? 20 : 24, // 字体大小
scrollBeyondLastLine: false, // 取消代码后面一大段空白 scrollBeyondLastLine: false, // 取消代码后面一大段空白
}) })
monaco.editor.getModels()[0].onDidChangeContent(() => { 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> </script>
<template> <template>
<el-form :inline="true"> <el-form inline>
<el-form-item label="语言" label-width="60"> <el-form-item label="语言" label-width="60">
<el-select v-model="state.language" class="language"> <el-select v-model="state.language" class="language">
<el-option <el-option
@@ -115,22 +87,11 @@ async function submit() {
<el-button @click="reset">重置</el-button> <el-button @click="reset">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div <section
ref="monacoEditorRef" ref="monacoEditorRef"
:class="isMobile ? 'editorMobile' : 'editor'" :class="isMobile ? 'editorMobile' : 'editor'"
></div> ></section>
<el-tabs type="border-card"> <EditorExec :state="state" :problem="problem" />
<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>
</template> </template>
<style scoped> <style scoped>
@@ -139,15 +100,11 @@ async function submit() {
} }
.editor { .editor {
height: 70%; /* 141px+400 */
height: calc(100vh - 541px);
} }
.editorMobile { .editorMobile {
height: 500px; height: 500px;
} }
.actions {
margin-top: 16px;
float: right;
}
</style> </style>

View File

@@ -13,7 +13,7 @@ const { data: problem, isFinished } = getProblem(problemID)
</script> </script>
<template> <template>
<el-row v-if="isFinished && problem"> <el-row v-if="isFinished && problem" :gutter="20">
<el-col :span="isDesktop ? 12 : 24"> <el-col :span="isDesktop ? 12 : 24">
<el-tabs type="border-card"> <el-tabs type="border-card">
<el-tab-pane label="题目描述"> <el-tab-pane label="题目描述">
@@ -29,17 +29,13 @@ const { data: problem, isFinished } = getProblem(problemID)
<el-tab-pane label="题目信息" lazy> <el-tab-pane label="题目信息" lazy>
<ProblemInfo :problem="problem" /> <ProblemInfo :problem="problem" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="提交情况">3</el-tab-pane> <el-tab-pane label="提交列表">3</el-tab-pane>
</el-tabs> </el-tabs>
</el-col> </el-col>
<el-col v-if="isDesktop" :span="12" class="editorWrapper"> <el-col v-if="isDesktop" :span="12">
<Editor :problem="problem" /> <Editor :problem="problem" />
</el-col> </el-col>
</el-row> </el-row>
</template> </template>
<style scoped> <style scoped></style>
.editorWrapper {
height: calc(100vh - 171px);
}
</style>

View File

@@ -100,7 +100,7 @@ onMounted(listProblems)
</script> </script>
<template> <template>
<el-form :inline="true"> <el-form inline>
<el-form-item label="难度"> <el-form-item label="难度">
<el-select v-model="query.difficulty"> <el-select v-model="query.difficulty">
<el-option <el-option

View File

@@ -1,8 +1,8 @@
import { useToggle } from "@vueuse/core"
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { ref } from "vue"
export const useSignupStore = defineStore("signup", () => { export const useSignupStore = defineStore("signup", () => {
const visible = ref(false) const [visible] = useToggle()
function show() { function show() {
visible.value = true visible.value = true

49
src/routes.ts Normal file
View 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

View 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>

View 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>

View 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>

View File

@@ -1,8 +1,8 @@
import { useToggle } from "@vueuse/core"
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { ref } from "vue"
export const useLoginStore = defineStore("login", () => { export const useLoginStore = defineStore("login", () => {
const visible = ref(false) const [visible] = useToggle()
function show() { function show() {
visible.value = true visible.value = true

View File

@@ -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 = { export const JUDGE_STATUS = {
"-2": { "-2": {
name: "Compile Error", name: "编译失败",
short: "CE", type: "error",
color: "yellow",
type: "warning",
}, },
"-1": { "-1": {
name: "Wrong Answer", name: "答案错误",
short: "WA",
color: "red",
type: "error", type: "error",
}, },
"0": { "0": {
name: "Accepted", name: "答案正确",
short: "AC",
color: "green",
type: "success", type: "success",
}, },
"1": { "1": {
name: "Time Limit Exceeded", name: "运行超时",
short: "TLE",
color: "red",
type: "error", type: "error",
}, },
"2": { "2": {
name: "Time Limit Exceeded", name: "运行超时",
short: "TLE",
color: "red",
type: "error", type: "error",
}, },
"3": { "3": {
name: "Memory Limit Exceeded", name: "内存超限",
short: "MLE",
color: "red",
type: "error", type: "error",
}, },
"4": { "4": {
name: "Runtime Error", name: "运行时错误",
short: "RE",
color: "red",
type: "error", type: "error",
}, },
"5": { "5": {
name: "System Error", name: "系统错误",
short: "SE",
color: "red",
type: "error", type: "error",
}, },
"6": { "6": {
name: "Pending", name: "等待评分",
color: "yellow",
type: "warning", type: "warning",
}, },
"7": { "7": {
name: "Judging", name: "正在评分",
color: "blue",
type: "info", type: "info",
}, },
"8": { "8": {
name: "Partial Accepted", name: "部分正确",
short: "PAC", type: "warning",
color: "blue",
type: "info",
}, },
"9": { "9": {
name: "Submitting", name: "正在提交",
color: "yellow", type: "info",
type: "warning",
}, },
} }

View File

@@ -38,3 +38,15 @@ export function parseTime(utc: Date, format = "YYYY年M月D日") {
const time = useDateFormat(utc, format, { locales: "zh-CN" }) const time = useDateFormat(utc, format, { locales: "zh-CN" })
return time.value 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"
}

View File

@@ -6,11 +6,10 @@ const http = axios.create({
xsrfCookieName: "csrftoken", xsrfCookieName: "csrftoken",
}) })
// TODO
http.interceptors.response.use( http.interceptors.response.use(
(res) => { (res) => {
if (res.data.error) { if (res.data.error) {
// 若后端返回为登录则为session失效应退出当前登录用户 // // TODO: 若后端返回为登录则为session失效应退出当前登录用户
if (res.data.data.startsWith("Please login")) { if (res.data.data.startsWith("Please login")) {
} }
return Promise.reject(res.data) return Promise.reject(res.data)

View File

@@ -7,6 +7,8 @@ export type LANGUAGE =
| "JavaScript" | "JavaScript"
| "Golang" | "Golang"
export type SUBMISSION_RESULT = -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
export interface Problem { export interface Problem {
_id: string _id: string
id: number id: number
@@ -56,3 +58,40 @@ export interface SubmitCodePayload {
code: string code: string
contest_id?: number 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
}