OJ判题项目-简易判题机(四)
往期开发(已经完成)
- OJ 判题项目-介绍(一)
- OJ 判题项目-初始化(二)
- OJ 判题项目-前后端联调(三)
- OJ 判题项目-简易判题机(四)
- OJ 判题项目-原生代码沙箱(五)
- OJ 判题项目-Docker 代码沙箱(六)
今日后端开发
判题模块架构
先模拟跑通正常的业务流程
- 判题模块和代码沙箱的关系
判题模块只需要调用代码沙箱执行代码 然后返回判题结果
代码沙箱只负责接收输入和输出用例执行代码返回执行结果 判题模块和代码沙箱之间实现解耦

为什么代码沙箱需要接收一组测试用例?
一般情况下测试时候会有很多个测试用例,比如力扣
每次测试都要调用一次接口吗?或者是说每次调用都要编译相同的代码?
所以要进行批量处理防止多次重复编译占用资源
- 判题模块和代码沙箱的关系
代码沙箱架构开发
定义代码沙箱结构,提高通用性
定义代码沙箱接口
1
2
3
4
5
6
7
8
9
10public interface CodeSandBox {
/**
* 提交代码接口
*
* @param executeCodeRequest
* @return
*/
ExecuteCodeResponse doExecuteCode(ExecuteCodeRequest executeCodeRequest);
}定义统一的请求类和返回类使用建造者模式来构建参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
481. 请求类
public class ExecuteCodeRequest {
/**
* 输入用例
*/
private List<String> inputList;
/**
* 语言
*/
private String language;
/**
* 代码
*/
private String code;
}
2. 结果类
public class ExecuteCodeResponse {
/**
* 输出用例
*/
private List<String> outputList;
/**
* 沙箱的运行信息
*/
private String message;
/**
* 判题状态
*/
private Integer status;
/**
* 判题信息
*/
private JudgeInfo judgeInfo;
}定义枚举类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public enum CodeSandBoxEnum {
/**
* 枚举值
*/
EXAMPLE("example"),
REMOTE("remote"),
THIRD_PARTY("thirdParty");
final private String name;
CodeSandBoxEnum(String name) {
this.name = name;
}
public String getValue() {
return name;
}
public static CodeSandBoxEnum getCodeSandBoxEnum(String type) {
switch (type) {
case "example":
return CodeSandBoxEnum.EXAMPLE;
case "remote":
return CodeSandBoxEnum.REMOTE;
case "thirdParty":
return CodeSandBoxEnum.THIRD_PARTY;
default:
return CodeSandBoxEnum.EXAMPLE;
}
}
}定义多种代码沙箱的实现类
例如1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* 示例沙箱
*/
public class ExampleCodeSandBox implements CodeSandBox {
public ExecuteCodeResponse doExecuteCode(ExecuteCodeRequest executeCodeRequest) {
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
executeCodeResponse.setOutputList(new ArrayList<>());
executeCodeResponse.setMessage("");
executeCodeResponse.setStatus(0);
executeCodeResponse.setJudgeInfo(new JudgeInfo());
return executeCodeResponse;
}
}远程代码沙箱,示例代码沙箱(模拟业务流程),第三方代码沙箱
代码沙箱日志优化(增强)
由于代码沙箱是第三方服务,每次调用可能会出现问题,所以要进行日志记录
采用代理模式来实现日志的打印记录每次调用的状态、请求参数、返回结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CodeSandBoxProxy implements CodeSandBox {
private final CodeSandBox codeSandBox;
public ExecuteCodeResponse doExecuteCode(ExecuteCodeRequest executeCodeRequest) {
log.info("代码沙箱的请求参数为:{}", executeCodeRequest.toString());
ExecuteCodeResponse executeCodeResponse = codeSandBox.doExecuteCode(executeCodeRequest);
log.info("代码沙箱的返回结果为:{}", executeCodeResponse.toString());
return executeCodeResponse;
}
}代码沙箱创建优化
由于有多个代码沙箱后续维护更换沙箱时候要去更改实例的对象。
这样维护成本高,还不高效,甚至会导致项目运行不起来
所以这里使用工厂模式+spring 配置的方式来创建对应的实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class CodeSandBoxProxyFactory {
/**
* 静态工厂
*
* @param type
* @return
*/
public static CodeSandBoxProxy newCreateSandBoxFactory(String type) {
CodeSandBoxEnum codeSandBoxEnum = CodeSandBoxEnum.getCodeSandBoxEnum(type);
switch (codeSandBoxEnum) {
case EXAMPLE:
return new CodeSandBoxProxy(new ExampleCodeSandBox());
case REMOTE:
return new CodeSandBoxProxy(new RemoteCodeSandBox());
case THIRD_PARTY:
return new CodeSandBoxProxy(new ThirdPartyCodeSandBox());
default:
throw new RuntimeException("不存在此类沙箱");
}
}
}1
2codesandbox:
type: remote使用读取配置类传入到工厂中得到对应的实体类
1
2
private String type;判题服务结果分析
由于代码沙箱执行编译,需要先指定编程语言,这就导致了每个编程语言都有不同的判题策略,
如果在返回结果时候进行判断将会有大量的 if-else 导致代码变得难读还不易维护
所以采用策略模式来优化针对于不同语言的处理策略
下面只举例 JAVA 其他语言实现方法一样1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
1301. 先策略内容实体类,再定义策略接口,然后传入策略内容
1.1 策略实体类
public class JudgeContext {
ExecuteCodeResponse executeCodeResponse;
private List<String> outputList;
private JudgeConfig judgeConfig;
}
1.2 策略接口
public interface JudgeStrategy {
/**
* 策略判断接口
*
* @param judgeContext
* @return
*/
JudgeInfo doCodeSandBoxJudge(JudgeContext judgeContext);
}
2. 定义策略实现类
public class JavaJudgeStrategyImpl implements JudgeStrategy {
public static final Long JAVA_RUNNING_TIME = 10000L;
private static final Long CHANGE_LIMIT = 1000L;
public JudgeInfo doCodeSandBoxJudge(JudgeContext judgeContext) {
// 判断输出用例是否与配置的用例长度或者是结果是否一致
ExecuteCodeResponse executeCodeResponse = judgeContext.getExecuteCodeResponse();
// 问题的系统配置
List<String> outputList = judgeContext.getOutputList();
JudgeConfig judgeConfig = judgeContext.getJudgeConfig();
// 沙箱运行执行返回的结果
List<String> codeSandBoxOutputList = executeCodeResponse.getOutputList();
JudgeInfo judgeInfo = executeCodeResponse.getJudgeInfo();
JudgeInfo resultJudgeInfo = new JudgeInfo();
resultJudgeInfo.setMemory(judgeInfo.getMemory());
resultJudgeInfo.setTime(judgeInfo.getTime());
// 判断输出是否错误
if (outputList.size() != codeSandBoxOutputList.size()) {
resultJudgeInfo.setMessage(JudgeQuestionEnum.WRONG_ANSWER.getValue());
return resultJudgeInfo;
}
for (int i = 0; i < outputList.size(); i++) {
if (!outputList.get(i).equals(codeSandBoxOutputList.get(i))) {
resultJudgeInfo.setMessage(JudgeQuestionEnum.WRONG_ANSWER.getValue());
return resultJudgeInfo;
}
}
// 判断时间问题
if (judgeConfig.getMemoryLimit() + JAVA_RUNNING_TIME < judgeInfo.getTime()) {
resultJudgeInfo.setMessage(JudgeQuestionEnum.TIME_LIMIT_EXCEEDED.getValue());
return resultJudgeInfo;
}
// 判断内存问题
if ((judgeConfig.getMemoryLimit() * CHANGE_LIMIT) < judgeInfo.getMemory()) {
resultJudgeInfo.setMessage(JudgeQuestionEnum.MEMORY_LIMIT_EXCEEDED.getValue());
return resultJudgeInfo;
}
resultJudgeInfo.setMessage(JudgeQuestionEnum.ACCEPTED.getValue());
return resultJudgeInfo;
}
}
3. 定义策略枚举类
public enum JudgeStrategyLanguageEnum {
/**
* 枚举值
*/
DEFAULT("default"),
JAVA("java"),
JAVA_SCRIPT("javascript"),
CPP("cpp");
final private String name;
JudgeStrategyLanguageEnum(String name) {
this.name = name;
}
public String getValue() {
return name;
}
public static JudgeStrategyLanguageEnum getJudgeStrategyLanguageEnum(String language) {
switch (language) {
case "default":
return JudgeStrategyLanguageEnum.DEFAULT;
case "java":
return JudgeStrategyLanguageEnum.JAVA;
case "javascript":
return JudgeStrategyLanguageEnum.JAVA_SCRIPT;
case "cpp":
return JudgeStrategyLanguageEnum.CPP;
default:
return JudgeStrategyLanguageEnum.DEFAULT;
}
}
}
4. 定义策略管理者JudgeStrategyManager来管理策略的选择
public class JudgeStrategyManager {
/**
* 选择策略
*
* @param language
* @param judgeContext
* @return
*/
public JudgeInfo getJudgeInfoByJudgeStrategy(String language, JudgeContext judgeContext) {
JudgeStrategyLanguageEnum judgeStrategyLanguageEnum = JudgeStrategyLanguageEnum.getJudgeStrategyLanguageEnum(language);
switch (judgeStrategyLanguageEnum) {
case JAVA:
return new JavaJudgeStrategyImpl().doCodeSandBoxJudge(judgeContext);
case JAVA_SCRIPT:
return new JavaScriptJudgeStrategyImpl().doCodeSandBoxJudge(judgeContext);
case CPP:
return new CppJudgeStrategyImpl().doCodeSandBoxJudge(judgeContext);
default:
return new DefaultJudgeStrategyImpl().doCodeSandBoxJudge(judgeContext);
}
}
}
判题服务开发
- 判题服务业务流程分析
- 传入题目的提交信息和题目信息
- 如果用户包含了正在运行或等待中的题目则返回提交失败
- 如果不存在则继续执行并更新状态为判题中 Running 防止重复执行
- 将题目信息和提交信息传递给代码沙箱服务,得到执行结果
- 根据结果更新提交信息
定义问题提交接口
1
2
3
4
5
6
7
8/**
* 问题提交
*
* @param questionSubmitAddRequest
* @param loginUser
* @return
*/
Long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser);实现问题提交接口
由于要调用第三方服务所以可以先提交到数据库中,让用户看到状态
然后使用异步的方式调用代码沙箱接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33/**
* 问题提交
*
* @param questionSubmitAddRequest
* @param loginUser
* @return
*/
public Long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser) {
Long questionId = questionSubmitAddRequest.getQuestionId();
Question question = questionService.getById(questionId);
ThrowUtils.throwIf(Objects.isNull(question), ErrorCode.OPERATION_ERROR, "问题不存在");
QueryWrapper<QuestionSubmit> questionSubmitQueryWrapper = new QueryWrapper<>();
questionSubmitQueryWrapper.eq("userId", loginUser.getId());
questionSubmitQueryWrapper.eq("questionId", questionId);
List<QuestionSubmit> isHaveQuestionSubmit = this.list(questionSubmitQueryWrapper);
if (isHaveQuestionSubmit.size() > 0) {
List<QuestionSubmit> collect = isHaveQuestionSubmit.stream().filter(questionSubmit -> questionSubmit.getStatus() == 1L || questionSubmit.getStatus() == 0L).collect(Collectors.toList());
ThrowUtils.throwIf(collect.size() > 0, ErrorCode.OPERATION_ERROR, "已经有任务存在了");
}
String language = questionSubmitAddRequest.getLanguage();
ThrowUtils.throwIf(QuestionSubmitLanguageEnum.getEnumByValue(language) == null, ErrorCode.OPERATION_ERROR, "编程语言错误");
QuestionSubmit questionSubmit = new QuestionSubmit();
BeanUtils.copyProperties(questionSubmitAddRequest, questionSubmit);
questionSubmit.setUserId(loginUser.getId());
boolean save = this.save(questionSubmit);
ThrowUtils.throwIf(!save, ErrorCode.SYSTEM_ERROR);
// todo 执行判题 异步执行
CompletableFuture.runAsync(() -> {
codeSandBoxService.doCodeSandBoxJudge(this.getById(questionSubmit.getId()), question);
});
return questionSubmit.getId();
}定义代码沙箱服务调用接口
1
2
3
4
5
6
7
8
9
10public interface CodeSandBoxService {
/**
* 执行代码沙箱判断接口
*
* @param questionSubmit
* @param question
* @return
*/
void doCodeSandBoxJudge(QuestionSubmit questionSubmit, Question question);
}实现代码沙箱服务调用接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class CodeSandBoxServiceImpl implements CodeSandBoxService {
private JudgeStrategyManager judgeStrategyManager;
private QuestionSubmitService questionSubmitService;
private QuestionService questionService;
private String type;
public void doCodeSandBoxJudge(QuestionSubmit questionSubmit, Question question) {
// 更新状态
QuestionSubmit questionSubmitFirstUpdate = new QuestionSubmit();
questionSubmitFirstUpdate.setId(questionSubmit.getId());
questionSubmitFirstUpdate.setStatus(QuestionStatusEnum.RUNNING.getValue());
boolean isUpdate = questionSubmitService.updateById(questionSubmitFirstUpdate);
ThrowUtils.throwIf(!isUpdate, ErrorCode.OPERATION_ERROR, "更新任务状态失败!");
// 构建沙箱执行请求体
String language = questionSubmit.getLanguage();
String code = questionSubmit.getCode();
String judgeCaseStr = question.getJudgeCase();
List<JudgeCase> questionJudgeCases = JSONUtil.toList(judgeCaseStr, JudgeCase.class);
List<String> inputList = questionJudgeCases.stream().map(JudgeCase::getInput).collect(Collectors.toList());
ThrowUtils.throwIf(inputList.size() == 0, ErrorCode.SYSTEM_ERROR);
// todo 沙箱请求结果
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code(code)
.language(language)
.inputList(inputList)
.build();
// todo 调用沙箱得到结果
CodeSandBoxProxy codeSandBoxProxy = CodeSandBoxProxyFactory.newCreateSandBoxFactory(type);
ExecuteCodeResponse executeCodeResponse = codeSandBoxProxy.doExecuteCode(executeCodeRequest);
// todo 封装策略上下文
JudgeContext judgeContext = new JudgeContext();
judgeContext.setExecuteCodeResponse(executeCodeResponse);
List<String> outputList = questionJudgeCases.stream().map(JudgeCase::getOutput).collect(Collectors.toList());
judgeContext.setOutputList(outputList);
String judgeConfig = question.getJudgeConfig();
JudgeConfig changeJudgeConfig = JSONUtil.toBean(judgeConfig, JudgeConfig.class);
judgeContext.setJudgeConfig(changeJudgeConfig);
// todo 调用策略来执行不同的语言判断逻辑返回JudgeInfo
JudgeInfo judgeInfoByJudgeStrategy = judgeStrategyManager.getJudgeInfoByJudgeStrategy(language, judgeContext);
// 更新题目状态
question.setSubmitNum(question.getSubmitNum() + 1);
if (Objects.equals(judgeInfoByJudgeStrategy.getMessage(), JudgeQuestionEnum.ACCEPTED.getValue())) {
question.setAcceptedNum(question.getAcceptedNum() + 1);
}
boolean b = questionService.updateById(question);
ThrowUtils.throwIf(!b, ErrorCode.OPERATION_ERROR, "更新题目状态失败!");
// 更新状态
QuestionSubmit questionSubmitSecondUpdate = new QuestionSubmit();
questionSubmitSecondUpdate.setId(questionSubmit.getId());
questionSubmitSecondUpdate.setStatus(QuestionStatusEnum.SUCCEED.getValue());
questionSubmitSecondUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfoByJudgeStrategy));
isUpdate = questionSubmitService.updateById(questionSubmitSecondUpdate);
ThrowUtils.throwIf(!isUpdate, ErrorCode.OPERATION_ERROR, "更新任务状态失败!");
}
}controller 接口定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 提交判题
*
* @param questionSubmitAddRequest
* @param request
* @return resultNum 本次点赞变化数
*/
public BaseResponse<Long> doQuestionSubmit( QuestionSubmitAddRequest questionSubmitAddRequest,
HttpServletRequest request) {
if (questionSubmitAddRequest == null || questionSubmitAddRequest.getQuestionId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
return ResultUtils.success(questionSubmitService.doQuestionSubmit(questionSubmitAddRequest, loginUser));
}
- 判题服务业务流程分析
今日前端开发
组件 Markdown 接入
推荐使用的 md 编辑器: https://github.com/bytedance/bytemd
- 安装
1
2npm i @bytemd/vue-next
npm i @bytemd/plugin-highlight @bytem/plugin-gfm- 安装完成,封装组件代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32<template>
<Editor
:value="props.value"
:plugins="plugins"
mode="split"
@change="props.handleChange"
/>
</template>
<script setup lang="ts">
import gfm from "@bytemd/plugin-gfm";
import { Editor } from "@bytemd/vue-next";
import { withDefaults, defineProps } from "vue";
import highlight from "@bytemd/plugin-highlight";
import frontmatter from "@bytemd/plugin-frontmatter";
interface Props {
value: string;
handleChange: (v: string) => void;
}
const plugins = [gfm(), highlight(), frontmatter()];
const props = withDefaults(defineProps<Props>(), {
value: "",
handleChange: (v: string) => {
console.log(v);
},
});
</script>
<style scoped></style>
代码编辑器 CodeEditor 接入
微软官方代码编辑器:https://github.com/microsoft/monaco-editor
- 安装
1
npm install monaco-editor
- 在 vue.config.js 配置 webpack 插件:
1
2
3
4
5
6
7
8const { 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());
},
});- 安装完成,封装组件代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67<template>
<div id="code-container" ref="docCodeRef" style="min-height: 80vh"></div>
</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 docCodeRef = ref();
const codeEdit = ref();
watch(
() => props.language,
() => {
if (codeEdit.value) {
monaco.editor.setModelLanguage(
toRaw(codeEdit.value).getModel(),
props.language
);
}
}
);
onMounted(() => {
if (!docCodeRef.value) {
return;
}
codeEdit.value = monaco.editor.create(docCodeRef.value, {
value: props.value,
language: props.language,
theme: "vs-dark",
folding: true, // 是否折叠
foldingHighlight: true, // 折叠等高线
foldingStrategy: "indentation", // 折叠方式 auto | indentation
showFoldingControls: "always", // 是否一直显示折叠 always | mouseover
disableLayerHinting: true, // 等宽优化
emptySelectionClipboard: false, // 空选择剪切板
selectionClipboard: false, // 选择剪切板
automaticLayout: true, // 自动布局
codeLens: false, // 代码镜头
scrollBeyondLastLine: false, // 滚动完最后一行后再滚动一屏幕
colorDecorators: true, // 颜色装饰器
accessibilitySupport: "off", // 辅助功能支持 "auto" | "off" | "on"
lineNumbers: "on", // 行号 取值: "on" | "off" | "relative" | "interval" | function
lineNumbersMinChars: 5, // 行号最小字符 number
readOnly: false, //是否只读 取值 true | false
});
codeEdit.value.onDidChangeModelContent(() => {
props.handleChange(toRaw(codeEdit.value).getValue());
});
});
</script>
<style scoped></style>
使用 openAPI 生成后端接口代码
1
openapi --input http://localhost:8081/api/v2/api-docs --output ./src/service/chenoj-api --client axios
由于使用的 Session 登录 所以启用 cookie 携带
1
2
3
4
5
6
7
8
9
10
11export const OpenAPI: OpenAPIConfig = {
BASE: "http://localhost:8092",
VERSION: "1.0",
WITH_CREDENTIALS: true,
CREDENTIALS: "include",
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};创建题目和更新题目页面开发,页面内容一致 可以复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311<template>
<div id="addQuestionView">
<a-card :title="title" hoverable :header-style="bodyStyle">
<a-form
:model="form"
:style="{ width: '70%', margin: '0 auto' }"
@submit="handleSubmit"
>
<a-form-item field="title" label="标题:">
<a-input v-model="form.title" placeholder="请输入问题标题" />
</a-form-item>
<a-form-item field="content" label="标签:">
<a-input-tag
v-model="form.tags"
placeholder="请输入标签"
:max-tag-count="4"
allow-clear
/>
</a-form-item>
<a-form-item field="content" label="内容:">
<MdEditor :value="form.content" :handle-change="handleContent" />
</a-form-item>
<a-form-item field="answer" label="答案:">
<MdEditor :value="form.answer" :handle-change="handleAnswer" />
</a-form-item>
<a-form-item
label="题目配置:"
:content-flex="false"
:merge-props="false"
>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item
field="judgeConfig.memoryLimit"
label="内存限制(kb):"
style="min-width: 500px"
>
<a-input-number
:min="0"
mode="button"
:style="{ width: '220px' }"
v-model="form.judgeConfig.memoryLimit"
placeholder="内存限制(kb)"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
field="judgeConfig.timeLimit"
label="时间限制(ms):"
style="min-width: 520px"
>
<a-input-number
:min="0"
mode="button"
:style="{ width: '220px' }"
v-model="form.judgeConfig.timeLimit"
placeholder="时间限制(ms)"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
field="judgeConfig.stackLimit"
label="堆栈限制(kb):"
style="min-width: 500px"
>
<a-input-number
:min="0"
mode="button"
style="width: 220px"
v-model="form.judgeConfig.stackLimit"
placeholder="堆栈内存限制(kb)"
/>
</a-form-item>
</a-col>
</a-row>
</a-form-item>
<a-form-item
label="测试用例:"
:content-flex="false"
:merge-props="false"
>
<a-form-item
v-for="(judgeCaseItem, index) of form.judgeCase"
:field="`judgeCaseItem[${index}].value`"
:label="`第${index + 1}组测试用例:`"
:key="index"
>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item field="judgeCase.input" no-style>
<a-input
v-model="judgeCaseItem.input"
placeholder="请输入测试输入用例"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="judgeCase.output" no-style>
<a-input
v-model="judgeCaseItem.output"
placeholder="请输入测试输出用例"
/>
</a-form-item>
</a-col>
</a-row>
<a-button
@click="handleDelete(index)"
:style="{ marginLeft: '10px' }"
type="dashed"
status="danger"
>删除用例
</a-button>
</a-form-item>
<div>
<a-button
type="dashed"
status="success"
style="margin-left: 30px"
@click="handleAdd"
>添加用例
</a-button>
</div>
</a-form-item>
<a-form-item>
<a-button
html-type="submit"
type="primary"
:loading="loading"
style="width: 180px; margin: 0 auto"
>{{ title }}
</a-button>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import MdEditor from "@/components/MdEditor.vue";
import { Message } from "@arco-design/web-vue";
import {
QuestionAddRequest,
QuestionControllerService,
} from "@/service/chenoj-api";
import { useRoute, useRouter } from "vue-router";
const bodyStyle = ref({
display: "flex",
alignContent: "center",
justifyContent: "center",
flexDirection: "column",
letterSpacing: "5px",
});
const route = useRoute();
const isUpdate = ref(false);
const title = ref("添加题目");
const questionId = ref<any>("");
const handleGetUpdateQuestion = async () => {
try {
const res = await QuestionControllerService.getQuestionByIdUsingGet(
questionId.value
);
if (res.code == 0) {
const data = res.data;
if (data) {
if (data.tags) {
data.tags = JSON.parse(data.tags);
}
if (data.judgeCase) {
data.judgeCase = JSON.parse(data.judgeCase);
} else {
data.judgeCase = "";
}
if (data.judgeConfig) {
data.judgeConfig = JSON.parse(data.judgeConfig);
} else {
data.judgeConfig = "";
}
if (data.answer == undefined) {
data.answer = "";
}
form.value = JSON.parse(JSON.stringify(data));
}
} else {
Message.error("请求数据失败!");
}
} catch (e) {
Message.error("请求失败,请重试!");
}
};
onMounted(() => {
const fullPath = route.fullPath.split("/");
if (fullPath[1] == "update") {
isUpdate.value = true;
title.value = "更新题目";
questionId.value = fullPath[2].split("=")[1];
handleGetUpdateQuestion();
}
});
// 监听页面 刷新表单状态
watch(
() => route.fullPath,
() => {
if (route.fullPath[1] !== "update") {
form.value = question;
}
}
);
const question = {
answer: "",
content: "",
judgeCase: [
{
input: "",
output: "",
},
],
judgeConfig: {
memoryLimit: 100,
stackLimit: 100,
timeLimit: 100,
},
tags: [],
title: "",
};
const form = ref(question);
const loading = ref(false);
const handleAnswer = (value: string) => {
form.value.answer = value;
};
const handleContent = (value: string) => {
form.value.content = value;
};
const handleAdd = () => {
if (form.value.judgeCase) {
if (form.value.judgeCase.length + 1 > 4) {
Message.warning("用例最多四个");
return;
}
form.value.judgeCase.push({
input: "",
output: "",
});
}
};
const handleDelete = (index: number) => {
if (form.value.judgeCase) {
if (form.value.judgeCase.length == 1) {
Message.warning("用例最少为一个");
return;
}
form.value.judgeCase.splice(index, 1);
}
};
const router = useRouter();
const handleSubmit = async () => {
try {
loading.value = true;
if (isUpdate.value) {
console.log(form);
const res = await QuestionControllerService.updateQuestionUsingPost(
form.value
);
if (res.code == 0) {
Message.success("更新成功!");
await router.push({
path: "/list/question",
replace: true,
});
} else {
Message.error("更新失败,", res.message);
}
} else {
const res = await QuestionControllerService.addQuestionUsingPost(
form.value
);
if (res.code == 0) {
Message.success("添加成功!");
form.value = question;
} else {
Message.error("添加失败,", res.message);
}
}
} catch (e) {
Message.error("请求失败,请稍后重试!");
}
loading.value = false;
};
</script>
<style scoped>
#addQuestionView {
}
</style>

路由结合权限
比如:
1
2
3
4
5
6
7
8{
path: "/list/submit/question",
name: "提交题目列表",
component: () => import("@/views/question/ListSubmitQuestionView.vue"),
meta: {
access: ACCESS.USER,
},
},题目列表开发
与 BI 项目一致引入 table 组件进行封装即可

题目浏览页开发
与 BI 项目一致引入 table 组件进行封装即可

