配置 webstorm prettier 设置,ctrl+alt+L 格式化代码
安装 acro design 并引入
npm install --save-dev @arco-design/web-vueimport { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import "@arco-design/web-vue/dist/arco.css";
import ArcoVue from "@arco-design/web-vue";
import store from "./store";
createApp(App).use(store).use(router).use(ArcoVue).mount("#app");实现通用布局
BasicLayout.vue
页面分三部分 header、content、footer
<template>
<div id="basicLayout">
<a-layout style="height: 400px">
<a-layout-header class="header">
<global-header />
</a-layout-header>
<a-layout-content class="content">
Content
<router-view />
</a-layout-content>
<a-layout-footer class="footer">Online Judge</a-layout-footer>
</a-layout>
</div>
</template>
<style>
#basicLayout {
}
#basicLayout .header {
margin-bottom: 16px;
}
#basicLayout .content {
margin-bottom: 16px;
}
#basicLayout .footer {
background: #efefef;
padding: 16px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
}
</style>
<script setup lang="ts">
import GlobalHeader from "@/components/GlobalHeader.vue";
</script>
实现通用菜单
GlobalHeader.vue
使用 a-menu 组件 Arco Design Vue
提取通用路由文件
根据路由自动创建菜单选项,页面自动高亮对应导航栏
点击导航栏自动路由到对应的页面
<template>
<div id="globalHeader">
<a-menu
mode="horizontal"
:selected-keys="selectedKeys"
@menu-item-click="doMenuClick"
>
<a-menu-item
key="0"
:style="{ padding: 0, marginRight: '38px' }"
disabled
>
<div class="title-bar">
<img class="logo" src="../assets/logo.png" />
<div class="title">OJ</div>
</div>
</a-menu-item>
<a-menu-item v-for="item in routes" :key="item.path"
>{{ item.name }}
</a-menu-item>
</a-menu>
</div>
</template>
<script setup lang="ts">
import { routes } from "@/router/routes";
import { useRoute, useRouter } from "vue-router";
import { ref } from "vue";
const router = useRouter();
// 默认主页
const selectedKeys = ref(["/"]);
// 路由跳转,更新选中的菜单项
router.afterEach((to, from, failure) => {
selectedKeys.value = [to.path];
});
const doMenuClick = (key: string) => {
router.push({
path: key,
});
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.title-bar {
display: flex;
align-items: center;
}
.logo {
height: 48px;
}
.title {
margin-left: 16px;
color: #444;
}
</style>全局状态管理
store\user.ts
先定义用户模块
import { StoreOptions } from "vuex";
export default {
namespace: true,
state: () => ({
loginUser: {
userName: "未登录",
},
}),
actions: {
getLoginUser({ commit, state }, payload) {
commit("updateUser", payload);
},
},
mutations: {
updateUser(state, payload) {
state.loginUser = payload;
},
},
} as StoreOptions<any>;store\index.ts 导出用户模块
import { createStore } from "vuex";
import user from "@/store/user";
export default createStore({
mutations: {},
actions: {},
modules: {
user,
},
});获取/修改状态变量
<a-col flex="100px">
<div>{{ store.state.user?.loginUser?.userName ?? "未登录" }}</div>
</a-col>
<script setup lang="ts">
import { useStore } from "vuex";
const store = useStore();
setTimeout(() => {
store.dispatch("getLoginUser", {
userName: "鱼皮",
});
}, 3000);
</script>全局权限校验
- 定义权限
access\accessEnum.ts
const ACCESS_ENUM = {
NOT_LOGIN: "notLogin",
USER: "user",
ADMIN: "admin",
};
export default ACCESS_ENUM;- 权限校验函数
access\checkAccess.ts
import ACCESS_ENUM from "@/access/accessEnum";
/**
* 检查权限
* @param loginUser 当前登录用户
* @param needAccess 需要有的权限
* @return boolean 有无权限
*/
const checkAccess = (loginUser: any, needAccess = ACCESS_ENUM.NOT_LOGIN) => {
const loginUserAccess = loginUser?.userRole ?? ACCESS_ENUM.NOT_LOGIN;
if (needAccess === ACCESS_ENUM.NOT_LOGIN) {
return true;
}
if (needAccess === ACCESS_ENUM.USER) {
if (loginUserAccess === ACCESS_ENUM.NOT_LOGIN) {
return false;
}
}
if (needAccess === ACCESS_ENUM.ADMIN) {
if (loginUserAccess !== ACCESS_ENUM.ADMIN) {
return false;
}
}
return true;
};
export default checkAccess;- 动态加载导航栏路由 GlobalHeader.vue
const visibleRoutes = computed(() => {
return routes.filter((item, index) => {
if (item.meta?.hideInMenu) {
return false;
}
if (
!checkAccess(store.state.user.loginUser, item?.meta?.access as string)
) {
return false;
}
return true;
});
});全局项目入口
// 全局初始化
const doInit = () => {
console.log("hello!");
};
onMounted(() => {
doInit();
});后端初始化
crtl+shfit+r 全局替换 spingbootinit 和 springboot-init
shift+f6 替换包名和项目名
前端请求
安装 axios、openapi
npm install axios@1.6.2
npm install openapi-typescript-codegen --save-dev生成代码
openapi --input http://localhost:8101/api/v2/api-docs --output ./generated --client axios
开启 cookie :
WITH_CREDENTIALS: true
自动登录
access\index.ts
import router from "@/router";
import store from "@/store";
import ACCESS_ENUM from "@/access/accessEnum";
import checkAccess from "@/access/checkAccess";
router.beforeEach(async (to, from, next) => {
// 获取用户登陆状态
let loginUser = store.state.user.loginUser;
if (!loginUser || !loginUser.userRole) {
await store.dispatch("getLoginUser");
loginUser = store.state.user.loginUser;
}
// 需要的权限
const needAccess = (to.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN;
// 如果需要登陆
if (needAccess !== ACCESS_ENUM.NOT_LOGIN) {
// 没登陆就跳转登录
if (
!loginUser ||
!loginUser.userRole ||
loginUser.userRole === ACCESS_ENUM.NOT_LOGIN
) {
next(`/user/login?rediect=${to.fullPath}`);
return;
}
// 登录了但没权限就跳转无权限
if (!checkAccess(loginUser, needAccess)) {
next("/noAuth");
return;
}
}
// 登陆了或者不需要权限的页面直接可以放行
next();
});多套布局
添加路由
{
path: "/user",
name: "用户",
component: UserLayout,
meta: {
hideInMenu: true,
},
children: [
{
path: "/user/login",
name: "用户登录",
component: UserLoginView,
},
{
path: "/user/register",
name: "用户注册",
component: UserRegisterView,
},
],
},新建 UserLayout.vue, UserLoginView, UserRegisterView
在 app.vue 中使用多套布局
<template v-if="route.path.startsWith('/user')">
<router-view />
</template>
<template v-else>
<basic-layout />
</template>登录页面
UserLoginView.vue
<template>
<div id="userLogin">
<a-form :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
<a-form-item field="userAccount" label="账号">
<a-input v-model="form.userAccount" placeholder="请输入用户名" />
</a-form-item>
<a-form-item field="userPassword" tooltip="密码不少于8位" label="密码">
<a-input-password
v-model="form.userPassword"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item>
<a-button html-type="submit">提交</a-button>
</a-form-item>
</a-form>
{{ form }}
</div>
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { UserControllerService, UserLoginRequest } from "../../../generated";
import message from "@arco-design/web-vue/es/message";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
const store = useStore();
const router = useRouter();
const form = reactive({
userAccount: "",
userPassword: "",
} as UserLoginRequest);
const handleSubmit = async () => {
const res = await UserControllerService.userLoginUsingPost(form);
if (res.code === 0) {
await store.dispatch("getLoginUser");
router.push({
path: "/",
replace: true,
});
} else {
message.error("登录失败" + res.message);
}
};
</script>Markdown 编辑器
pd4d10/bytemd: ByteMD v1 repository
安装组件库
npm i @bytemd/vue-next安装插件
npm i @bytemd/plugin-highlight
npm i @bytemd/plugin-math
npm i @bytemd/plugin-gfm组件属性
interface Props {
value: string;
handleChange: (v: string) => void;
}MdEditor.vue
<template>
<Editor :value="value" :plugins="plugins" @change="handleChange" />
</template>
<script setup lang="ts">
import highlight from "@bytemd/plugin-highlight";
import math from "@bytemd/plugin-math";
import { Editor, Viewer } from "@bytemd/vue-next";
import { ref, withDefaults, defineProps } from "vue";
import "katex/dist/katex.css";
interface Props {
value: string;
handleChange: (v: string) => void;
}
const plugins = [
highlight(),
math(),
// Add more plugins here
];
const props = withDefaults(defineProps<Props>(), {
value: () => "",
handleChange: (v: string) => {
console.log(v);
},
});
</script>
<style scoped></style>代码编辑器
安装组件(遇到编译问题安装第三个包):
npm install monaco-editor
npm install monaco-editor-webpack-plugin
npm install @babel/plugin-transform-class-static-block加载配置 vue.config.js
const { defineConfig } = require("@vue/cli-service");
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack(config) {
config.plugin("monaco").use(new MonacoWebpackPlugin());
},
});babel.config.js
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: ["@babel/plugin-transform-class-static-block"],
};
codeEditor.vue
<template>
<div id="code-editor"
style="min-height: 400px; height: 70vh"
ref="codeEditorRef"
/>
</template>
<script setup lang="ts">
import * as monaco from "monaco-editor";
import { defineProps, onMounted, ref, toRaw, watch, withDefaults } from "vue";
interface Props {
value: string;
language: string;
handleChange: (v: string) => void;
}
const props = withDefaults(defineProps<Props>(), {
value: () => "",
language: () => "java",
handleChange: (v: string) => {
console.log(v);
},
});
const codeEditorRef = ref();
const codeEditor = ref();
watch(
() => props.language,
() => {
if (codeEditor.value) {
monaco.editor.setModelLanguage(
toRaw(codeEditor.value.getModel()),
props.language
);
}
}
);
onMounted(() => {
if (!codeEditorRef.value) {
return;
}
codeEditor.value = monaco.editor.create(codeEditorRef.value, {
value: props.value,
language: props.language,
minimap: {
enabled: true,
},
readOnly: false,
theme: "vs-dark",
});
codeEditor.value.onDidChangeModelContent(() => {
props.handleChange(toRaw(codeEditor.value).getValue());
});
});
</script>
<style scoped></style>创建题目页面
嵌套表单:chttps://arco.design/vue/component/form#nest
动态增减表单:https://arco.design/vue/component/form#dynamic
分页问题:@page-change事件,改变searchParams 值,通过 watchEffect 监听 searchParmas 然后执行 loadData 重新加载
<template>
<div id="addQuestionView">
<h2>创建题目</h2>
<a-form :model="form">
<a-form-item field="title" label="标题">
<a-input v-model="form.title" />
</a-form-item>
<a-form-item field="content" label="题目内容">
<md-editor :value="form.content" :handle-change="onContentChange" />
</a-form-item>
<a-form-item field="tags" label="标签">
<a-input-tag v-model="form.tags" allow-clear />
</a-form-item>
<a-form-item field="answer" label="答案">
<md-editor :value="form.answer" :handle-change="onAnswerChange" />
</a-form-item>
<a-form-item label="判题配置" :content-flex="false" :merge-props="false">
<a-space direction="vertical" style="min-width: 480px">
<a-form-item field="judgeConfig.timeLimit" label="时间限制">
<a-input-number
v-model="form.judgeConfig.timeLimit"
placeholder="请输入时间限制"
mode="button"
min="0"
size="large"
/>
</a-form-item>
<a-form-item field="judgeConfig.memoryLimit" label="内存限制">
<a-input-number
v-model="form.judgeConfig.memoryLimit"
placeholder="请输入内存限制"
mode="button"
min="0"
size="large"
/>
</a-form-item>
<a-form-item field="judgeConfig.stackLimit" label="栈空间限制">
<a-input-number
v-model="form.judgeConfig.stackLimit"
placeholder="请输入栈空间限制"
mode="button"
min="0"
size="large"
/>
</a-form-item>
</a-space>
</a-form-item>
<a-form-item
field="judgeCase"
label="判题用例"
:content-flex="false"
:merge-props="false"
>
<a-form-item
v-for="(judgeCaseItem, index) of form.judgeCase"
:key="index"
style="min-width: 800px"
no-style
>
<a-space direction="vertical" style="min-width: 480px">
<a-form-item
:field="`form.judgeCase[${index}].input`"
:label="`输入用例-${index}`"
:key="index"
>
<a-input v-model="judgeCaseItem.input" placeholder="输入用例" />
</a-form-item>
<a-form-item
:field="`form.judgeCase[${index}].output`"
:label="`输出用例-${index}`"
:key="index"
>
<a-input v-model="judgeCaseItem.output" placeholder="输出用例" />
</a-form-item>
<a-button @click="deleteJudgeCase(index)" status="danger"
>删除
</a-button>
</a-space>
</a-form-item>
<div>
<a-button
@click="addJudgeCase"
type="outline"
status="success"
style="margin-top: 16px"
>添加测试用例
</a-button>
</div>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="doSubmit"
>提交
</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import { reactive } from "vue";
import MdEditor from "@/components/MdEditor.vue";
import {
QuestionAddRequest,
QuestionControllerService,
} from "../../generated";
import message from "@arco-design/web-vue/es/message";
const form = reactive({
answer: "print(a+b)",
content: "题目内容",
judgeCase: [
{
input: "1 2",
output: "3",
},
],
judgeConfig: {
timeLimit: 1000,
memoryLimit: 1000,
stackLimit: 1000,
},
tags: ["简单"],
title: "A+B",
});
const onContentChange = (v: string) => {
form.content = v;
};
const onAnswerChange = (v: string) => {
form.answer = v;
};
const addJudgeCase = () => {
form.judgeCase.push({
input: "",
output: "",
});
};
const deleteJudgeCase = (index: number) => {
form.judgeCase.splice(index, 1);
};
const doSubmit = async () => {
console.log(form);
const res = await QuestionControllerService.addQuestionUsingPost(form);
if (res.code === 0) {
message.success("创建成功");
} else {
message.error("创建失败" + res.message);
}
};
</script>
<style scoped></style>管理题目页面
表格渲染 https://arco.design/vue/component/table#custom
删除后需要手动调用 loadData 刷新数据
修改数据通过路由跳转到修改页
<template>
<div id="manageQuestionView">
<a-table
:columns="columns"
:data="dataList"
:pagination="{
showTotal: true,
pageSize: searchParams.pageSize,
current: searchParams.current,
total,
}"
@page-change="onPageChange"
>
<template #optional="{ record }">
<a-space>
<a-button @click="doUpdate(record)" type="primary">修改</a-button>
<a-button @click="doDelete(record)" status="danger">删除</a-button>
</a-space>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect } from "vue";
import { Question, QuestionControllerService } from "../../generated";
import message from "@arco-design/web-vue/es/message";
import { useRouter } from "vue-router";
const router = useRouter();
const dataList = ref([]);
const total = ref(0);
const searchParams = ref({
pageSize: 10,
current: 1,
});
const loadData = async () => {
const res = await QuestionControllerService.listQuestionByPageUsingPost(
searchParams.value
);
if (res.code === 0) {
dataList.value = res.data.records;
total.value = res.data.total;
} else {
message.error("加载失败," + res.message);
}
};
onMounted(() => {
loadData();
});
const doDelete = async (question: Question) => {
const res = await QuestionControllerService.deleteQuestionUsingPost(question);
if (res.code === 0) {
message.success("删除成功");
loadData();
} else {
message.error("删除失败," + res.message);
}
};
const doUpdate = (question: Question) => {
router.push({
path: "/update/question",
query: {
id: question.id,
},
});
};
watchEffect(() => {
loadData();
});
const onPageChange = (page: number) => {
searchParams.value = {
...searchParams.value,
current: page,
};
};
const columns = [
{
title: "id",
dataIndex: "id",
},
{
title: "标题",
dataIndex: "title",
},
{
title: "内容",
dataIndex: "content",
},
{
title: "标签",
dataIndex: "tags",
},
{
title: "答案",
dataIndex: "answer",
},
{
title: "判题样例",
dataIndex: "judgeCase",
},
{
title: "判题配置",
dataIndex: "judgeConfig",
},
{
title: "提交数",
dataIndex: "submitNum",
},
{
title: "通过数",
dataIndex: "acceptedNum",
},
{
title: "创建用户Id",
dataIndex: "userId",
},
{
title: "操作",
slotName: "optional",
},
];
</script>
<style scoped></style>修改题目页面
<template>
...
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import MdEditor from "@/components/MdEditor.vue";
import { QuestionControllerService } from "../../generated";
import message from "@arco-design/web-vue/es/message";
import { useRoute } from "vue-router";
const route = useRoute();
const updatePage = route.path.includes("update");
const form = ref({
answer: "题目答案",
content: "题目内容",
judgeCase: [
{
input: "1 2",
output: "3",
},
],
judgeConfig: {
timeLimit: 1000,
memoryLimit: 1000,
stackLimit: 1000,
},
tags: ["简单"],
title: "A+B",
});
const loadData = async () => {
const id = route.query.id;
if (!id) {
return;
}
const res = await QuestionControllerService.getQuestionVoByIdUsingGet(
id as any
);
if (res.code === 0) {
form.value = res.data as any;
} else {
message.error("加载失败," + res.message);
}
};
onMounted(() => {
loadData();
});
const onContentChange = (v: string) => {
form.value.content = v;
};
const onAnswerChange = (v: string) => {
form.value.answer = v;
};
const addJudgeCase = () => {
form.value.judgeCase.push({
input: "",
output: "",
});
};
const deleteJudgeCase = (index: number) => {
form.value.judgeCase.splice(index, 1);
};
const doSubmit = async () => {
if (updatePage) {
const res = await QuestionControllerService.updateQuestionUsingPost(
form.value
);
if (res.code === 0) {
message.success("更新成功");
} else {
message.error("更新失败," + res.message);
}
} else {
const res = await QuestionControllerService.addQuestionUsingPost(
form.value
);
if (res.code === 0) {
message.success("创建成功");
} else {
message.error("创建失败," + res.message);
}
}
};
</script>
<style scoped></style>git 设置
git config --global core.atuocrlf false
浏览题目页面
- 使用表格布局和表单作为搜索
- 自定义渲染,使用插槽渲染标签、通过率、创建时间
- 时间字符串工具类
moment
在线做题页面
路由中开启 props : true
组件:a-tab-pane a-description a-tag a-select
题目提交列表页面
检索项:
- status
- userId
- QuestionId
- language