OJ 模块开发
04-模块开发
用户提交问卷信息查询
1.后端接口开发(todo)
扩展思考:后端接口响应慢的优化思路
后端接口响应慢的优化思路:用逻辑操作替换频繁调用service请求(网络数据库交互)
两端代码的分析:前者是一次性获取所有的内容,然后做逻辑处理;后者是每次遍历都访问一次数据库
2.前端组件开发
开发前端页面:
1)用户注册页面
2)创建题目页面(管理员)
3)题目管理页面(管理员)
- 查看(搜索)
- 删除
- 修改
- 快捷创建
4)题目列表页(用户)
5)题目详情页(在线做题页)
- 判题状态的查看
6)题目提交列表页
组件接入
先接入可能用到的组件,再去写页面,避免因为后续依赖冲突、整合组件失败带来的返工
1)Markdown编辑器:bytemd
使用Markdown的原因:bytemd编辑器(结合其官方文档Usage引入内容)
- 引入样式文件
- 引入MD组件(选择Vue版本)
一套通用的文本编辑语法,可以在各大网站上统一标准、渲染出统一的样式、比较简单易学。
推荐的MD编辑器:阅读官方文档、下载编辑器主体、以及gfm(表格支持)插件、highlight代码高亮插件。
npm i @bytemd/vue-next
npm i @bytemd/plugin-highlight @bytemd/plugin-gfm
MD编辑器引入
(1)main.ts中引入样式文件
import 'bytemd/dist/index.css'
(2)在compontents中创建F引入MEditor.vue组件
<template>
<Editor :value="value" :plugins="plugins" @change="handleChange" />
</template>
<script setup lang="ts">
import gfm from "@bytemd/plugin-gfm";
import highlight from "@bytemd/plugin-highlight";
import { Editor, Viewer } from "@bytemd/vue-next";
import { ref } from "vue";
const plugins = [
gfm(),
highlight(),
// Add more plugins here
];
const value = ref("");
const handleChange = (v: string) => {
value.value = v;
};
</script>
<style scoped></style>
(3)router/routers.ts中配置路由、启动项目查看组件效果
{
path: "/mdEditor",
name: "MD编辑器",
component: () => import("../components/MdEditor.vue"),
},
可以设定隐藏编辑器中不需要的图标(比如GitHub图标)
.bytemd-toolbar-icon.bytemd-tippy.bytemd-tippy-right:last-child {
display: none;
}
把MdEditor当前输入的值暴露给父组件,便于父组件去使用,同时也是提高组件的通用性,需要定义属性,把value和handleChange事件交给父组件去管理:
例如在HomeView.vue中引入MdEditor组件
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<MdEditor></MdEditor>
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
import MdEditor from "@/components/MdEditor.vue"; // @ is an alias to /src
export default defineComponent({
name: "HomeView",
components: {
MdEditor,
HelloWorld,
},
});
</script>
父组件控制MdEditor(提高MdEditor组件通用性)
MdEditor 示例代码:(让父组件去控制MdEditor内部的状态,还有一种思路是通过v-model实现),基于这种方式是的MdEditor更具备通用性。有一种思路是在父组件中用户点击提交的时候拿到子组件MdEditor的value值,这种最简单粗暴,但是相对来说那么灵活
MdEditor改造(vue父组件操作子组件的一个基础实现方式)
# 父组件控制子组件核心思路(Props传递属性、withDefaults设定默认值(当父组件不指定的时候设定默认值))
/**
* 定义组件属性类型
*/
interface Props {
value: string;
handleChange: (v: string) => void;
}
/**
* 给组件指定初始值
*/
const props = withDefaults(defineProps<Props>(), {
value: () => "",
handleChange: (v: string) => {
console.log(v);
},
});
# 参考改造内容
<template>
<Editor :value="value" :plugins="plugins" @change="handleChange" />
</template>
<script setup lang="ts">
import gfm from "@bytemd/plugin-gfm";
import highlight from "@bytemd/plugin-highlight";
import { Editor, Viewer } from "@bytemd/vue-next";
import { ref } from "vue";
/**
* 定义组件属性类型
*/
interface Props {
value: string;
handleChange: (v: string) => void;
}
const plugins = [
gfm(),
highlight(),
// Add more plugins here
];
/**
* 给组件指定初始值
*/
const props = withDefaults(defineProps<Props>(), {
value: () => "",
handleChange: (v: string) => {
console.log(v);
},
});
</script>
<style scoped></style>
HomeView改造(注意script 加上setup属性)
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<MdEditor :value="value" :handle-change="onChange"></MdEditor>
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script setup lang="ts">
import { defineComponent, ref } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
import MdEditor from "@/components/MdEditor.vue"; // @ is an alias to /src
const value = ref("");
const onChange = (v: string) => {
value.value = v;
};
</script>
测试:输入内容查看联动
2)代码编辑器:monaco-editor
整合步骤:
vue-cli项目整合monaco-editor
【1】安装编辑器
npm install monaco-editor
【2】 vue-cli 项目(webpack 项目)整合 monaco-editor
先安装monaco-ediror-webpack-plugin:
npm i monaca-editor-webpack-plugin
# 如果安装失败(提示404)使用下述指令
npm install monaco-editor-webpack-plugin --save-dev
【3】在vue.config.js中配置webpack插件:(因为vue-cli对webpack集成做了整合,它提供了一个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());
},
});
- 按需加载方式
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
module.exports = {
chainWebpack: config => {
config.plugin('monaco-editor').use(MonacoWebpackPlugin, [
{
// Languages are loaded on demand at runtime
languages: ['json', 'go', 'css', 'html', 'java', 'javascript', 'less', 'markdown', 'mysql', 'php', 'python', 'scss', 'shell', 'redis', 'sql', 'typescript', 'xml'], // ['abap', 'apex', 'azcli', 'bat', 'cameligo', 'clojure', 'coffee', 'cpp', 'csharp', 'csp', 'css', 'dart', 'dockerfile', 'ecl', 'fsharp', 'go', 'graphql', 'handlebars', 'hcl', 'html', 'ini', 'java', 'javascript', 'json', 'julia', 'kotlin', 'less', 'lexon', 'lua', 'm3', 'markdown', 'mips', 'msdax', 'mysql', 'objective-c', 'pascal', 'pascaligo', 'perl', 'pgsql', 'php', 'postiats', 'powerquery', 'powershell', 'pug', 'python', 'r', 'razor', 'redis', 'redshift', 'restructuredtext', 'ruby', 'rust', 'sb', 'scala', 'scheme', 'scss', 'shell', 'solidity', 'sophia', 'sql', 'st', 'swift', 'systemverilog', 'tcl', 'twig', 'typescript', 'vb', 'xml', 'yaml'],
features: ['format', 'find', 'contextmenu', 'gotoError', 'gotoLine', 'gotoSymbol', 'hover' , 'documentSymbols'] //['accessibilityHelp', 'anchorSelect', 'bracketMatching', 'caretOperations', 'clipboard', 'codeAction', 'codelens', 'colorPicker', 'comment', 'contextmenu', 'coreCommands', 'cursorUndo', 'dnd', 'documentSymbols', 'find', 'folding', 'fontZoom', 'format', 'gotoError', 'gotoLine', 'gotoSymbol', 'hover', 'iPadShowKeyboard', 'inPlaceReplace', 'indentation', 'inlineHints', 'inspectTokens', 'linesOperations', 'linkedEditing', 'links', 'multicursor', 'parameterHints', 'quickCommand', 'quickHelp', 'quickOutline', 'referenceSearch', 'rename', 'smartSelect', 'snippets', 'suggest', 'toggleHighContrast', 'toggleTabFocusMode', 'transpose', 'unusualLineTerminators', 'viewportSemanticTokens', 'wordHighlighter', 'wordOperations', 'wordPartOperations']
}
])
}
}
此处选择方式:
如何使用
示例教程:Monco Editor
整合教程参考:Monaco Editor安装及使用
注意,monaco editor 在读写值的时候,要使用toRaw(编辑器实例)的语法来执行操作,否则会卡死。
1)在compontent新建一个CodeEditor.vue(代码编辑器),示例整合代码如下:
<template>
<div id="code-editor" ref="codeEditorRef" style="min-height: 400px" />
{{ value }}
<a-button @click="fillValue">填充值</a-button>
</template>
<script setup lang="ts">
import * as monaco from "monaco-editor";
import { onMounted, ref, toRaw } from "vue";
const codeEditorRef = ref();
const codeEditor = ref();
const value = ref("hello world");
const fillValue = () => {
if (!codeEditor.value) {
return;
}
// 改变值
toRaw(codeEditor.value).setValue("新的值");
};
onMounted(() => {
if (!codeEditorRef.value) {
return;
}
codeEditor.value = monaco.editor.create(codeEditorRef.value, {
value: value.value,
language: "java",
automaticLayout: true,
colorDecorators: true,
minimap: {
enabled: true,
scale: 5,
},
readOnly: false,
theme: "vs-dark",
});
// 编辑 监听内容变化
codeEditor.value.onDidChangeModelContent(() => {
console.log("目前内容为:", toRaw(codeEditor.value).getValue());
});
});
</script>
<style scoped></style>
2)配置路由router/routes.ts:测试CodeEditor
{
path: "/codeEditor",
name: "Code编辑器",
component: () => import("../components/CodeEditor.vue"),
},
3)启动项目测试(如果出错参考下述整合报错问题解决)
整合报错问题解决:
Module build failed (from ./node_modules/babel-loader/lib/index.js): SyntaxError: E:\workspace\Git\github\PROJECT\noob\oj-platform\oj-platform-frontend\node_modules\monaco-editor\esm\vs\language\typescript\tsMode.js: Static class blocks are not enabled. Please add `@babel/plugin-transform-class-static-block` to your configuration.
(1)提示配置确认插件,根据提示安装插件
npm install @babel/plugin-transform-class-static-block
(2)进入 Babel 配置文件(.babelrc
或 babel.config.js
),加入下方代码:
plugins: ["@babel/plugin-transform-class-static-block"],
(3)重启项目确认
这里有个点:编辑器输入内容,但是不会输出(这个组件不会像MdEditor那样自动设置联动更新结果),需要手动调整结果值
父组件控制CodeEditor
和上面的Md 编辑器一样,让组件更具备通用性。接受父组件的传值,把显示的输入交给父组件去控制,从而能够让父组件实时得到用户输入的代码:
/**
* 定义组件属性类型
*/
interface Props {
value: string;
handleChange: (v: string) => void;
}
/**
* 给组件指定初始值
*/
const props = withDefaults(defineProps<Props>(), {
value: () => "",
handleChange: (v: string) => {
console.log(v);
},
});
参考CodeEditor实现:
<template>
<div
id="code-editor"
ref="codeEditorRef"
style="min-height: 400px; height: 60vh"
/>
<!-- <a-button @click="fillValue">填充值</a-button>-->
</template>
<script setup lang="ts">
import * as monaco from "monaco-editor";
import { onMounted, ref, toRaw, withDefaults, defineProps, watch } 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();
// const fillValue = () => {
// if (!codeEditor.value) {
// return;
// }
// // 改变值
// toRaw(codeEditor.value).setValue("新的值");
// };
watch(
() => props.language,
() => {
if (codeEditor.value) {
monaco.editor.setModelLanguage(
toRaw(codeEditor.value).getModel(),
props.language
);
}
}
);
onMounted(() => {
if (!codeEditorRef.value) {
return;
}
// Hover on each property to see its docs!
codeEditor.value = monaco.editor.create(codeEditorRef.value, {
value: props.value,
language: props.language,
automaticLayout: true,
colorDecorators: true,
minimap: {
enabled: true,
},
readOnly: false,
theme: "vs-dark",
// lineNumbers: "off",
// roundedSelection: false,
// scrollBeyondLastLine: false,
});
// 编辑 监听内容变化
codeEditor.value.onDidChangeModelContent(() => {
props.handleChange(toRaw(codeEditor.value).getValue());
});
});
</script>
<style scoped></style>
父组件引入配置实现(view下创建一个ExampleView.vue示例,测试组件的应用)
{
path: "/exampleEditor",
name: "编辑器示例",
component: () => import("../views/ExampleView.vue"),
},
- ExampleView.vue示例
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<MdEditor :value="mdValue" :handle-change="mdOnChange"></MdEditor>
<CodeEditor :value="codeValue" :handle-change="codeOnChange"></CodeEditor>
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script setup lang="ts">
import { defineComponent, ref } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
import MdEditor from "@/components/MdEditor.vue";
import CodeEditor from "@/components/CodeEditor.vue"; // @ is an alias to /src
const mdValue = ref("");
const mdOnChange = (v: string) => {
mdValue.value = v;
};
const codeValue = ref("");
const codeOnChange = (v: string) => {
codeValue.value = v;
};
</script>
测试效果如下所示:
to:项目扩展
用diff editor对比用户代码和标准答案的区别
前端模板生成小技巧
新建一个vue文件,可以指定自定义的vue模板生成(便于快速开发)
配置模板内容:
指定模板生效的作用域:(例如此处指定为vue文件)
使用:新建一个Vue文件,然后清空文件内容,输入myvuepage
关键字按下回车生成模板
3.页面开发
后端接口编写完成,随后执行指令生成相应的service(此处注意一个小坑,每次生成新的服务,默认OpenAPI.ts中的WITH_CREDENTIALS(请求携带cookies)配置都是false,因此要手动设定其为true)
export const OpenAPI: OpenAPIConfig = {
BASE: "http://localhost:8101",
VERSION: "1.0",
WITH_CREDENTIALS: true, // 设置请求携带cookies
CREDENTIALS: "include",
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};
为了解决这个问题,可以在axios.ts中进行全局配置,避免每次生成新的接口文档都受到影响(todo)
# vue生成openapi文档指令
openapi --input http://localhost:8101/api/v2/api-docs --output ./generated --client axios
如果生成新的接口服务启动后项目报错,则去检查OpenAPI的配置信息(后端接口路径、请求携带cookies配置)
创建题目
此处需注意自定义的代码编辑器组件不会被组件库识别,需要手动去指定 value 和 handleChange 函数
参考用户输入值:
{
"answer": "暴力破解",
"content": "题目内容",
"judgeCase": [
{
"input": "1 2",
"output": "3 4"
}
],
"judgeConfig": {
"memoryLimit": 1000,
"stackLimit": 1000,
"timeLimit": 1000
},
"tags": [
"栈", "简单"
],
"title": "A + B"
}
构建步骤
1)新建views/question/AddQuestionView.vue
<template>
<div id="addQuestionView">
<h2>创建题目</h2>
<a-form :model="form" label-align="left">
<a-form-item field="title" label="标题">
<a-input v-model="form.title" placeholder="请输入标题" />
</a-form-item>
<a-form-item field="tags" label="标签">
<a-input-tag v-model="form.tags" placeholder="请选择标签" allow-clear />
</a-form-item>
<a-form-item field="content" label="题目内容">
<MdEditor :value="form.content" :handle-change="onContentChange" />
</a-form-item>
<a-form-item field="answer" label="答案">
<MdEditor :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
label="测试用例配置"
:content-flex="false"
:merge-props="false"
>
<a-form-item
v-for="(judgeCaseItem, index) of form.judgeCase"
:key="index"
no-style
>
<a-space direction="vertical" style="min-width: 640px">
<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 status="danger" @click="handleDelete(index)">
删除
</a-button>
</a-space>
</a-form-item>
<div style="margin-top: 32px">
<a-button @click="handleAdd" type="outline" status="success"
>新增测试用例
</a-button>
</div>
</a-form-item>
<div style="margin-top: 16px" />
<a-form-item>
<a-button type="primary" style="min-width: 200px" @click="doSubmit"
>提交
</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import { onMounted, 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();
// 如果页面地址包含 update,视为更新页面
const updatePage = route.path.includes("update");
let form = ref({
title: "",
tags: ["简单","入门","测试"],
answer: "",
content: "",
judgeConfig: {
memoryLimit: 1000,
stackLimit: 1000,
timeLimit: 1000,
},
judgeCase: [
{
input: "",
output: "",
},
],
});
/**
* 根据题目 id 获取老的数据
*/
const loadData = async () => {
const id = route.query.id;
if (!id) {
return;
}
const res = await QuestionControllerService.getQuestionByIdUsingGet(
id as any
);
if (res.code === 0) {
form.value = res.data as any;
// json 转 js 对象
if (!form.value.judgeCase) {
form.value.judgeCase = [
{
input: "",
output: "",
},
];
} else {
form.value.judgeCase = JSON.parse(form.value.judgeCase as any);
}
if (!form.value.judgeConfig) {
form.value.judgeConfig = {
memoryLimit: 1000,
stackLimit: 1000,
timeLimit: 1000,
};
} else {
form.value.judgeConfig = JSON.parse(form.value.judgeConfig as any);
}
if (!form.value.tags) {
form.value.tags = [];
} else {
form.value.tags = JSON.parse(form.value.tags as any);
}
} else {
message.error("加载失败," + res.message);
}
};
onMounted(() => {
loadData();
});
const doSubmit = async () => {
console.log(form.value);
// 区分更新还是创建
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);
}
}
};
/**
* 新增判题用例
*/
const handleAdd = () => {
form.value.judgeCase.push({
input: "",
output: "",
});
};
/**
* 删除判题用例
*/
const handleDelete = (index: number) => {
form.value.judgeCase.splice(index, 1);
};
const onContentChange = (value: string) => {
form.value.content = value;
};
const onAnswerChange = (value: string) => {
form.value.answer = value;
};
</script>
<style scoped>
#addQuestionView {
}
</style>
2)前面的接口实现都是比较简单的CRUD,现在要根据业务逻辑去处理请求(确认请求参数并处理)
构建步骤
涉及到JSON转化,需要引入gson组件(修改pom.xml)
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.1</version>
</dependency>
1)修改QuestionAddRequest请求参数:将tags、judgeCase、judgeConfig与前端定义的数据类型进行匹配(其中JudgeCase、JudgeConfig存储为json数据,可定义对应实体进行匹配)
@Data
public class QuestionAddRequest implements Serializable {
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/**
* 题目答案
*/
private String answer;
/**
* 判题用例
*/
private List<JudgeCase> judgeCase;
/**
* 判题配置
*/
private JudgeConfig judgeConfig;
private static final long serialVersionUID = 1L;
}
public class JudgeCase {
/**
* 输入用例
*/
private String input;
/**
* 输出用例
*/
private String output;
}
public class JudgeConfig {
/**
* 时间限制(ms)
*/
private Long timeLimit;
/**
* 内存限制(KB)
*/
private Long memoryLimit;
/**
* 堆栈限制(KB)
*/
private Long stackLimit;
}
2)修改QuestionController的add方法
@RestController
@RequestMapping("/question")
@Slf4j
public class QuestionController {
@Resource
private QuestionService questionService;
@Resource
private UserService userService;
private final static Gson GSON = new Gson();
/**
* 创建
* @param questionAddRequest
* @param request
* @return
*/
@PostMapping("/add")
public BaseResponse<Long> addQuestion(@RequestBody QuestionAddRequest questionAddRequest, HttpServletRequest request) {
if (questionAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Question question = new Question();
BeanUtils.copyProperties(questionAddRequest, question);
// 校验问题信息
questionService.validQuestion(question, true);
User loginUser = userService.getLoginUser(request);
question.setCreater(loginUser.getId());
question.setUpdater(loginUser.getId());
question.setCreateTime(new Date());
question.setUpdateTime(new Date());
// 设置标签
List<String> tags = questionAddRequest.getTags();
if (tags != null) {
question.setTags(GSON.toJson(tags));
}
// 设置测试用例
List<JudgeCase> judgeCase = questionAddRequest.getJudgeCase();
if (judgeCase != null) {
question.setJudgeCase(GSON.toJson(judgeCase));
}
// 设置题目配置
JudgeConfig judgeConfig = questionAddRequest.getJudgeConfig();
if (judgeConfig != null) {
question.setJudgeConfig(GSON.toJson(judgeConfig));
}
question.setFavourNum(0);
question.setThumbNum(0);
// 新增
boolean result = questionService.save(question);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
long newQuestionId = question.getId();
return ResultUtils.success(newQuestionId);
}
}
3)后端修改完成需要重启,然后前端生成API接口(如果请求参数和路径不变则可以不用重新生成)
其中标签输入规则,设定输入标签然后点击enter键则可创建标签
表单组件默认有初始化值,这块的内容可以定位具体的代码实现进行调整,后续业务逻辑还要继续优化这块的实现
题目管理
构建步骤
1)创建views/question/ManageQuestion.vue文件