refactor axios.

This commit is contained in:
2023-01-08 14:42:54 +08:00
parent 7bea386dbc
commit e306e6b463
16 changed files with 270 additions and 113 deletions

99
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@element-plus/icons-vue": "^2.0.10", "@element-plus/icons-vue": "^2.0.10",
"@monaco-editor/loader": "^1.3.2", "@monaco-editor/loader": "^1.3.2",
"@vueuse/core": "^9.10.0", "@vueuse/core": "^9.10.0",
"@vueuse/integrations": "^9.10.0",
"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",
@@ -778,6 +779,86 @@
} }
} }
}, },
"node_modules/@vueuse/integrations": {
"version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/integrations/-/integrations-9.10.0.tgz",
"integrity": "sha512-MLGVbN3i9gRq3pb8VRZXgPvbNJcUUvgR5pmbc1QZj4Z1vvsvxam159AwWEJdyX2I39a1E7EkmBujtiXtVckO5g==",
"dependencies": {
"@vueuse/core": "9.10.0",
"@vueuse/shared": "9.10.0",
"vue-demi": "*"
},
"peerDependencies": {
"async-validator": "*",
"axios": "*",
"change-case": "*",
"drauu": "*",
"focus-trap": "*",
"fuse.js": "*",
"idb-keyval": "*",
"jwt-decode": "*",
"nprogress": "*",
"qrcode": "*",
"universal-cookie": "*"
},
"peerDependenciesMeta": {
"async-validator": {
"optional": true
},
"axios": {
"optional": true
},
"change-case": {
"optional": true
},
"drauu": {
"optional": true
},
"focus-trap": {
"optional": true
},
"fuse.js": {
"optional": true
},
"idb-keyval": {
"optional": true
},
"jwt-decode": {
"optional": true
},
"nprogress": {
"optional": true
},
"qrcode": {
"optional": true
},
"universal-cookie": {
"optional": true
}
}
},
"node_modules/@vueuse/integrations/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": { "node_modules/@vueuse/metadata": {
"version": "9.10.0", "version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.10.0.tgz", "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.10.0.tgz",
@@ -2639,6 +2720,24 @@
} }
} }
}, },
"@vueuse/integrations": {
"version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/integrations/-/integrations-9.10.0.tgz",
"integrity": "sha512-MLGVbN3i9gRq3pb8VRZXgPvbNJcUUvgR5pmbc1QZj4Z1vvsvxam159AwWEJdyX2I39a1E7EkmBujtiXtVckO5g==",
"requires": {
"@vueuse/core": "9.10.0",
"@vueuse/shared": "9.10.0",
"vue-demi": "*"
},
"dependencies": {
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"requires": {}
}
}
},
"@vueuse/metadata": { "@vueuse/metadata": {
"version": "9.10.0", "version": "9.10.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.10.0.tgz", "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.10.0.tgz",

View File

@@ -13,6 +13,7 @@
"@element-plus/icons-vue": "^2.0.10", "@element-plus/icons-vue": "^2.0.10",
"@monaco-editor/loader": "^1.3.2", "@monaco-editor/loader": "^1.3.2",
"@vueuse/core": "^9.10.0", "@vueuse/core": "^9.10.0",
"@vueuse/integrations": "^9.10.0",
"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",

View File

@@ -21,6 +21,7 @@ const routes = [
{ {
path: "problem/:problemID", path: "problem/:problemID",
component: () => import("./oj/problem/detail.vue"), component: () => import("./oj/problem/detail.vue"),
props: true,
}, },
{ {
path: "status", path: "status",
@@ -30,6 +31,7 @@ const routes = [
{ {
path: "status/:statusID", path: "status/:statusID",
component: () => import("./oj/status/detail.vue"), component: () => import("./oj/status/detail.vue"),
props: true,
}, },
{ {
path: "contest", path: "contest",
@@ -39,6 +41,12 @@ const routes = [
{ {
path: "contest/:contestID", path: "contest/:contestID",
component: () => import("./oj/contest/detail.vue"), component: () => import("./oj/contest/detail.vue"),
props: true,
},
{
path: "contest/:contestID/problem/:problemID",
component: () => import("./oj/problem/detail.vue"),
props: true,
}, },
{ {
path: "rank", path: "rank",

View File

@@ -1,12 +1,14 @@
import { getACRate } from "./../utils/functions" import { getACRate } from "./../utils/functions"
import { DIFFICULTY } from "./../utils/constants" import { DIFFICULTY } from "./../utils/constants"
import { Problem, LANGUAGE } from "./../utils/types"
import http from "./../utils/http" import http from "./../utils/http"
import { useAxios } from "@vueuse/integrations/useAxios"
function filterResult(result: any) { function filterResult(result: Problem) {
const newResult: any = { const newResult: any = {
displayID: result._id, _id: result._id,
title: result.title, title: result.title,
difficulty: DIFFICULTY[<"Low" | "Mid" | "High">result.difficulty], difficulty: DIFFICULTY[result.difficulty],
tags: result.tags, tags: result.tags,
submission: result.submission_number, submission: result.submission_number,
rate: getACRate(result.accepted_number, result.submission_number), rate: getACRate(result.accepted_number, result.submission_number),
@@ -46,7 +48,7 @@ export async function getProblemList(
} }
export function getProblemTagList() { export function getProblemTagList() {
return http.get("problem/tags") return useAxios<{ id: number; name: string }[]>("problem/tags", http)
} }
export function getRandomProblemID() { export function getRandomProblemID() {
@@ -54,11 +56,20 @@ export function getRandomProblemID() {
} }
export function getProblem(id: string) { export function getProblem(id: string) {
return http.get("problem", { return useAxios<Problem>("problem", { params: { problem_id: id } }, http)
params: { problem_id: id }, }
export function getSubmission(id: string) {
return http.get("submission", {
params: { id },
}) })
} }
export function getWebsite() { export function submitCode(data: {
return http.get("website") problem_id: number
contest_id?: number
language: LANGUAGE
code: string
}) {
return http.post("submission", data)
} }

View File

@@ -35,11 +35,11 @@ onMounted(userStore.getMyProfile)
<el-menu-item index="/status">提交</el-menu-item> <el-menu-item index="/status">提交</el-menu-item>
<el-menu-item index="/rank">排名</el-menu-item> <el-menu-item index="/rank">排名</el-menu-item>
</el-menu> </el-menu>
<div v-if="userStore.isLoaded && !userStore.isAuthed" class="actions"> <div v-if="userStore.isFinished && !userStore.isAuthed" class="actions">
<el-button @click="loginStore.show">登录</el-button> <el-button @click="loginStore.show">登录</el-button>
<el-button @click="signupStore.show">注册</el-button> <el-button @click="signupStore.show">注册</el-button>
</div> </div>
<div v-if="userStore.isLoaded && userStore.isAuthed" class="actions"> <div v-if="userStore.isFinished && userStore.isAuthed" class="actions">
<el-dropdown @command="handleDropdown"> <el-dropdown @command="handleDropdown">
<el-button>{{ userStore.user.username }}</el-button> <el-button>{{ userStore.user.username }}</el-button>
<template #dropdown> <template #dropdown>

View File

@@ -1,29 +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 } from "vue" import { ref, onBeforeUnmount, onMounted, watch, reactive, computed } from "vue"
import { import {
LANGUAGE,
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"
const { problem } = defineProps<{ const { problem, contestID = "" } = defineProps<{
problem: { contestID?: string
languages: Array<LANGUAGE> problemID?: string
template: { [key in LANGUAGE]?: string } problem: Problem
}
}>() }>()
const state = reactive({ const state = reactive({
values: ref({ ...SOURCES }), values: ref({ ...SOURCES }),
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() { function reset() {
@@ -34,9 +33,7 @@ function reset() {
} }
} }
onMounted(() => { onMounted(init)
init()
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
monaco.editor.getModels().forEach((model) => model.dispose()) monaco.editor.getModels().forEach((model) => model.dispose())
@@ -77,6 +74,29 @@ async function init() {
state.values[state.language] = monaco.editor.getModels()[0].getValue() state.values[state.language] = 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 = {
problem_id: problem.id,
language: state.language,
code: state.values[state.language],
}
if (contestID) {
//@ts-ignore
data.contest_id = parseInt(contestID)
}
const res = await submitCode(data)
state.submissionId = res.data.submission_id
}
</script> </script>
<template> <template>
@@ -107,7 +127,9 @@ async function init() {
<el-form class="actions"> <el-form class="actions">
<el-form-item> <el-form-item>
<el-button>运行</el-button> <el-button>运行</el-button>
<el-button type="primary">提交</el-button> <el-button type="primary" :disabled="submitDisabled" @click="submit">
提交
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>

View File

@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const { problem } = defineProps(["problem"]) import { Problem } from "../../../utils/types"
const { problem } = defineProps<{ problem: Problem }>()
</script> </script>
<template> <template>

View File

@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { DIFFICULTY } from "../../../utils/constants" import { DIFFICULTY } from "../../../utils/constants"
import { getACRate, getTagColor } from "../../../utils/functions" import { getACRate, getTagColor, parseTime } from "../../../utils/functions"
import { isDesktop } from "../../../utils/breakpoints" import { isDesktop } from "../../../utils/breakpoints"
import { Problem } from "../../../utils/types"
const { problem } = defineProps(["problem"]) const { problem } = defineProps<{ problem: Problem }>()
</script> </script>
<template> <template>
@@ -14,19 +15,20 @@ const { problem } = defineProps(["problem"])
<el-descriptions-item label="出题人"> <el-descriptions-item label="出题人">
{{ problem.created_by.username }} {{ problem.created_by.username }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="难度"> <el-descriptions-item label="创建时间">
<el-tag disable-transitions :type="getTagColor(problem.difficulty)"> {{ parseTime(problem.create_time) }}
{{ DIFFICULTY[<"Low" | "Mid" | "High">problem.difficulty] }}
</el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="时间限制"> <el-descriptions-item label="时间限制">
{{ problem.time_limit }}毫秒 {{ problem.time_limit }}毫秒
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="内存限制"> <el-descriptions-item label="内存限制">
{{ problem.memory_limit }}MB {{ problem.memory_limit }}MB
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="类型"> <el-descriptions-item label="难度">
{{ problem.rule_type }} <el-tag disable-transitions :type="getTagColor(problem.difficulty)">
{{ DIFFICULTY[problem.difficulty] }}
</el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="提交正确"> <el-descriptions-item label="提交正确">

View File

@@ -1,39 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from "vue"
import { useRoute } from "vue-router"
import Editor from "./components/editor.vue" import Editor from "./components/editor.vue"
import ProblemContent from "./components/problem-content.vue" import ProblemContent from "./components/problem-content.vue"
import ProblemInfo from "./components/problem-info.vue" import ProblemInfo from "./components/problem-info.vue"
import { getProblem } from "../api" import { getProblem } from "../api"
import { isDesktop, isMobile } from "../../utils/breakpoints" import { isDesktop, isMobile } from "../../utils/breakpoints"
const route = useRoute() const { problemID = "", contestID = "" } = defineProps<{
const contestID = route.params.contestID as string problemID?: string
const problemID = route.params.problemID as string contestID?: string
}>()
const problem = ref({ const { data: problem, isFinished } = getProblem(problemID)
_id: "",
created_by: {},
io_mode: {},
languages: [],
samples: [],
statistic_info: {},
tags: [],
template: {},
})
async function init() {
const res = await getProblem(problemID)
problem.value = res.data
}
onMounted(() => {
init()
})
</script> </script>
<template> <template>
<el-row v-if="problem._id"> <el-row v-if="isFinished && problem">
<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="题目描述">

View File

@@ -17,9 +17,10 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const problems = ref([]) const problems = ref([])
const tags = ref(<{ id: number; name: string }[]>[])
const total = ref(0) const total = ref(0)
const { data: tags } = getProblemTagList()
const query = reactive({ const query = reactive({
keyword: route.query.keyword || "", keyword: route.query.keyword || "",
difficulty: route.query.difficulty || "", difficulty: route.query.difficulty || "",
@@ -28,11 +29,6 @@ const query = reactive({
limit: parseInt(<string>route.query.limit) || 10, limit: parseInt(<string>route.query.limit) || 10,
}) })
async function listTags() {
const res = await getProblemTagList()
tags.value = res.data
}
async function listProblems() { async function listProblems() {
query.keyword = route.query.keyword || "" query.keyword = route.query.keyword || ""
query.difficulty = route.query.difficulty || "" query.difficulty = route.query.difficulty || ""
@@ -77,7 +73,7 @@ async function getRandom() {
} }
function goProblem(row: any) { function goProblem(row: any) {
router.push("/problem/" + row.displayID) router.push("/problem/" + row._id)
} }
watch(() => query.page, routePush) watch(() => query.page, routePush)
@@ -98,12 +94,9 @@ watch(
) )
// TODO: 这里会在登录时候执行两次有BUG // TODO: 这里会在登录时候执行两次有BUG
watch(() => userStore.isLoaded && userStore.isAuthed, listProblems) watch(() => userStore.isFinished && userStore.isAuthed, listProblems)
onMounted(() => { onMounted(listProblems)
listTags()
listProblems()
})
</script> </script>
<template> <template>
@@ -158,11 +151,7 @@ onMounted(() => {
/></el-icon> /></el-icon>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="_id" label="编号" :width="isDesktop ? 100 : 60" />
prop="displayID"
label="编号"
:width="isDesktop ? 100 : 60"
/>
<el-table-column prop="title" label="标题" /> <el-table-column prop="title" label="标题" />
<el-table-column label="难度" width="100"> <el-table-column label="难度" width="100">
<template #default="scope"> <template #default="scope">

View File

@@ -1,7 +1,10 @@
import { useAxios } from "@vueuse/integrations/useAxios"
import http from "../utils/http" import http from "../utils/http"
export function login(data: { username: string; password: string }) { export function login(data: { username: string; password: string }) {
return http.post("login", data) return useAxios("login", { method: "post", data }, http, {
immediate: false,
})
} }
export function logout() { export function logout() {
@@ -9,7 +12,7 @@ export function logout() {
} }
export function getUserInfo(username: string) { export function getUserInfo(username: string) {
return http.get("profile", { return useAxios("profile", { method: "get", params: { username } }, http, {
params: { username }, immediate: false,
}) })
} }

View File

@@ -1,5 +1,5 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { computed, ref } from "vue" import { computed } from "vue"
import { import {
PROBLEM_PERMISSION, PROBLEM_PERMISSION,
STORAGE_KEY, STORAGE_KEY,
@@ -9,8 +9,7 @@ import storage from "../../utils/storage"
import { getUserInfo } from "../api" import { getUserInfo } from "../api"
export const useUserStore = defineStore("user", () => { export const useUserStore = defineStore("user", () => {
const profile = ref<any>({}) const { data: profile, isFinished, execute } = getUserInfo("")
const isLoaded = ref(false)
const user = computed(() => profile.value.user || {}) const user = computed(() => profile.value.user || {})
const isAuthed = computed(() => !!user.value.email) const isAuthed = computed(() => !!user.value.email)
const isAdminRole = computed( const isAdminRole = computed(
@@ -26,10 +25,7 @@ export const useUserStore = defineStore("user", () => {
) )
async function getMyProfile() { async function getMyProfile() {
isLoaded.value = false await execute()
const res = await getUserInfo("")
isLoaded.value = true
profile.value = res.data || {}
storage.set(STORAGE_KEY.AUTHED, !!user.value.email) storage.set(STORAGE_KEY.AUTHED, !!user.value.email)
} }
@@ -39,7 +35,7 @@ export const useUserStore = defineStore("user", () => {
} }
return { return {
profile, profile,
isLoaded, isFinished,
user, user,
isAdminRole, isAdminRole,
isSuperAdmin, isSuperAdmin,

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance } from "element-plus" import { FormInstance } from "element-plus"
import { reactive, ref } from "vue" import { computed, reactive, ref } from "vue"
import { useSignupStore } from "../../oj/stores/signup" import { useSignupStore } from "../../oj/stores/signup"
import { login } from "../../shared/api" import { login } from "../../shared/api"
import { useLoginStore } from "../stores/login" import { useLoginStore } from "../stores/login"
@@ -9,8 +9,6 @@ import { useUserStore } from "../stores/user"
const loginStore = useLoginStore() const loginStore = useLoginStore()
const signupStore = useSignupStore() const signupStore = useSignupStore()
const userStore = useUserStore() const userStore = useUserStore()
const loading = ref(false)
const errorMessage = ref("")
const loginRef = ref<FormInstance>() const loginRef = ref<FormInstance>()
const form = reactive({ const form = reactive({
username: "", username: "",
@@ -23,23 +21,19 @@ const rules = reactive({
{ min: 6, max: 20, message: "长度在6到20位之间", trigger: "change" }, { min: 6, max: 20, message: "长度在6到20位之间", trigger: "change" },
], ],
}) })
const { isLoading, error, execute } = login(form)
const msg = computed(() => error.value && "用户名或密码不正确")
async function submit() { async function submit() {
if (!loginRef.value) return if (!loginRef.value) return
await loginRef.value.validate(async (valid) => { const valid = await loginRef.value.validate()
if (valid) { if (valid) {
loading.value = true await execute()
errorMessage.value = "" if (!error.value) {
try { loginStore.hide()
await login(form) userStore.getMyProfile()
loginStore.hide()
await userStore.getMyProfile()
} catch (err) {
errorMessage.value = "用户名或密码不正确"
}
loading.value = false
} }
}) }
} }
function goSignup() { function goSignup() {
@@ -75,17 +69,12 @@ function goSignup() {
></el-input> ></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="loading" @click="submit" <el-button type="primary" :loading="isLoading" @click="submit">
>登录</el-button 登录
> </el-button>
<el-button @click="goSignup">没有账号立即注册</el-button> <el-button @click="goSignup">没有账号立即注册</el-button>
</el-form-item> </el-form-item>
<el-alert <el-alert v-if="msg" :title="msg" show-icon type="error" />
v-if="errorMessage"
:title="errorMessage"
show-icon
type="error"
/>
</el-form> </el-form>
</el-dialog> </el-dialog>
</template> </template>

View File

@@ -196,8 +196,6 @@ export const LANGUAGE_LABEL = {
Golang: "Go", Golang: "Go",
} }
export type LANGUAGE = keyof typeof LANGUAGE_LABEL
export const LANGUAGE_VALUE = { export const LANGUAGE_VALUE = {
C: "c", C: "c",
"C++": "cpp", "C++": "cpp",

View File

@@ -1,3 +1,4 @@
import { useDateFormat } from "@vueuse/core"
import { STORAGE_KEY } from "./constants" import { STORAGE_KEY } from "./constants"
export function getACRate(acCount: number, totalCount: number) { export function getACRate(acCount: number, totalCount: number) {
@@ -32,3 +33,8 @@ export function getTagColor(tag: string) {
: "danger", : "danger",
}[tag] }[tag]
} }
export function parseTime(utc: Date, format = "YYYY年M月D日") {
const time = useDateFormat(utc, format, { locales: "zh-CN" })
return time.value
}

51
src/utils/types.ts Normal file
View File

@@ -0,0 +1,51 @@
export type LANGUAGE =
| "C"
| "C++"
| "Python2"
| "Python3"
| "Java"
| "JavaScript"
| "Golang"
export interface Problem {
_id: string
id: number
tags: string[]
created_by: {
id: number
username: string
real_name: null
}
template: { [key in LANGUAGE]?: string }
title: string
description: string
input_description: string
output_description: string
samples: {
input: string
output: string
}[]
hint: string
languages: Array<LANGUAGE>
create_time: Date
last_update_time: null
time_limit: number
memory_limit: number
io_mode: {
input: string
output: string
io_mode: string
}
spj: boolean
spj_language: null
rule_type: string
difficulty: "Low" | "Mid" | "High"
source: string
total_score: number
submission_number: number
accepted_number: number
statistic_info: {}
share_submission: boolean
contest: null
my_status: number
}