修改前端编辑器样式以及md格式转换

This commit is contained in:
Himit_ZH 2021-02-22 01:28:56 +08:00
parent 0b3ced6193
commit 702e450df9
59 changed files with 1411 additions and 1150 deletions

View File

@ -7,8 +7,10 @@
> 当前任务
- [x] 测试HDU判题整套流程
- [ ] 修改前端编辑器样式以及md格式转换
- [x] 修改前端编辑器样式以及md格式转换
- [ ] 修复代码编辑器bug
- [ ] 测试比赛相关接口,验证权限及数据计算
- [ ] 增加codeforce的vj判题
- [ ] 部署判题服务器到云服务器半正式上线HOJ
- [ ] 完善文档
@ -40,6 +42,7 @@
| 2021-02-16 | 完善测试特殊判题题目的操作 | Himit_ZH |
| 2021-02-19 | 正式完善HDU虚拟判题以及题目添加 | Himit_ZH |
| 2021-02-20 | 测试HDU判题整套流程完成 | Himit_ZH |
| 2021-02-22 | 修改前端编辑器样式以及md格式转换 | Himit_ZH |
# 二、系统架构

View File

@ -12,6 +12,7 @@ import top.hcode.hoj.pojo.entity.CompileSpj;
import top.hcode.hoj.pojo.entity.Judge;
import top.hcode.hoj.pojo.entity.ToJudge;
import top.hcode.hoj.service.ToJudgeService;
import top.hcode.hoj.service.impl.JudgeServiceImpl;
import top.hcode.hoj.utils.Constants;
import top.hcode.hoj.utils.RedisUtils;
@ -36,11 +37,19 @@ public class CloudHandler implements ToJudgeService {
@Autowired
private RedisUtils redisUtils;
@Autowired
private JudgeServiceImpl judgeService;
// 调度判题服务器失败可能是判题服务器有故障或者全部达到判题最大数那么将该提交重新进入等待队列
@Override
public CommonResult submitProblemJudge(ToJudge toJudge) {
if (toJudge.getTryAgainNum() == 40) {
Judge judge = toJudge.getJudge();
judge.setStatus(Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus());
judge.setErrorMessage("Failed to connect the judgeServer. Please resubmit this submission again!");
judgeService.updateById(judge);
} else {
// 线程沉睡1秒再将任务重新发布避免过快问题同时判题服务过多导致的失败
try {
TimeUnit.SECONDS.sleep(1);
@ -48,7 +57,9 @@ public class CloudHandler implements ToJudgeService {
e.printStackTrace();
}
Judge judge = toJudge.getJudge();
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), toJudge.getToken(), judge.getCid() == 0);
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), toJudge.getToken(),
judge.getCid() == 0, toJudge.getTryAgainNum() + 1);
}
return CommonResult.errorResponse("判题服务器繁忙或出错,提交进入重判队列,请等待管理员处理!", CommonResult.STATUS_ERROR);
}
@ -66,16 +77,24 @@ public class CloudHandler implements ToJudgeService {
account.set("password", toJudge.getPassword());
redisUtils.llPush(Constants.Judge.getListNameByOJName(toJudge.getRemoteJudge().split("-")[0]), JSONUtil.toJsonStr(account));
if (toJudge.getTryAgainNum() == 40) {
Judge judge = toJudge.getJudge();
judge.setStatus(Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus());
judge.setErrorMessage("Failed to connect the judgeServer. Please resubmit this submission again!");
judgeService.updateById(judge);
} else {
// 线程沉睡一秒再将任务重新发布避免过快问题同时判题服务过多导致的失败
try {
TimeUnit.SECONDS.sleep(2);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Judge judge = toJudge.getJudge();
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), toJudge.getToken(),
toJudge.getRemoteJudge(), judge.getCid() == 0);
toJudge.getRemoteJudge(), judge.getCid() == 0, toJudge.getTryAgainNum() + 1);
}
return CommonResult.errorResponse("判题服务器繁忙或出错,提交进入重判队列,请等待管理员处理!", CommonResult.STATUS_ERROR);
}

View File

@ -25,7 +25,8 @@ public class CorsConfig implements WebMvcConfigurer {
// 前端直接通过/public/img/图片名称即可拿到
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(Constants.File.USER_AVATAR_API.getPath()+"**") // /public/img/**
.addResourceLocations("file:" + Constants.File.USER_AVATAR_FOLDER.getPath());
registry.addResourceHandler(Constants.File.IMG_API.getPath()+"**") // /public/img/**
.addResourceLocations("file:" + Constants.File.USER_AVATAR_FOLDER.getPath(),
"file:" + Constants.File.MARKDOWN_IMG_FOLDER.getPath());
}
}

View File

@ -1,8 +1,8 @@
package top.hcode.hoj.config;
import com.netflix.loadbalancer.IRule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.hcode.hoj.judge.self.JudgeChooseRule;
/**
@ -10,7 +10,7 @@ import top.hcode.hoj.judge.self.JudgeChooseRule;
* @Date: 2021/2/4 23:10
* @Description:
*/
@Slf4j
@Configuration
public class RibbonConfig {
@Bean

View File

@ -101,9 +101,10 @@ public class AdminJudgeController {
// 调用判题服务
Problem problem = problemService.getById(judge.getPid());
if (problem.getIsRemote()) { // 如果是远程oj判题
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, problem.getProblemId(), judge.getCid() == 0);
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, problem.getProblemId(),
judge.getCid() == 0, 1);
} else {
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, judge.getCid() == 0);
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, judge.getCid() == 0, 1);
}
return CommonResult.successResponse(judge, "重判成功!该提交已进入判题队列!");
} else {
@ -148,12 +149,13 @@ public class AdminJudgeController {
if (problem.getIsRemote()) { // 如果是远程oj判题
for (Judge judge : rejudgeList) {
// 进入重判队列等待调用判题服务
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, problem.getProblemId(), judge.getCid() == 0);
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, problem.getProblemId(),
judge.getCid() == 0, 1);
}
} else {
for (Judge judge : rejudgeList) {
// 进入重判队列等待调用判题服务
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, judge.getCid() == 0);
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, judge.getCid() == 0, 1);
}
}

View File

@ -142,7 +142,7 @@ public class FileController {
//更新user_info里面的avatar
UpdateWrapper<UserInfo> userInfoUpdateWrapper = new UpdateWrapper<>();
userInfoUpdateWrapper.set("avatar", Constants.File.USER_FILE_HOST.getPath() + Constants.File.USER_AVATAR_API.getPath() + filename)
userInfoUpdateWrapper.set("avatar", Constants.File.USER_FILE_HOST.getPath() + Constants.File.IMG_API.getPath() + filename)
.eq("uuid", userRolesVo.getUid());
userInfoService.update(userInfoUpdateWrapper);
@ -158,7 +158,7 @@ public class FileController {
.put("uid", userRolesVo.getUid())
.put("username", userRolesVo.getUsername())
.put("nickname", userRolesVo.getNickname())
.put("avatar", Constants.File.USER_FILE_HOST.getPath() + Constants.File.USER_AVATAR_API.getPath() + filename)
.put("avatar", Constants.File.USER_FILE_HOST.getPath() + Constants.File.IMG_API.getPath() + filename)
.put("email", userRolesVo.getEmail())
.put("number", userRolesVo.getNumber())
.put("school", userRolesVo.getSchool())
@ -515,4 +515,60 @@ public class FileController {
return "txt";
}
@RequestMapping(value = "/upload-md-img", method = RequestMethod.POST)
@RequiresAuthentication
@ResponseBody
@Transactional
@RequiresRoles(value = {"root", "admin"}, logical = Logical.OR)
public CommonResult uploadMDImg(@RequestParam("image") MultipartFile image) {
if (image == null) {
return CommonResult.errorResponse("图片为空!");
}
if (image.getSize() > 1024 * 1024 * 4) {
return CommonResult.errorResponse("上传的图片文件大小不能大于4M");
}
//获取文件后缀
String suffix = image.getOriginalFilename().substring(image.getOriginalFilename().lastIndexOf(".") + 1);
if (!"jpg,jpeg,gif,png,webp".toUpperCase().contains(suffix.toUpperCase())) {
return CommonResult.errorResponse("请选择jpg,jpeg,gif,png,webp格式的图片");
}
File savePathFile = new File(Constants.File.MARKDOWN_IMG_FOLDER.getPath());
if (!savePathFile.exists()) {
//若不存在该目录则创建目录
savePathFile.mkdir();
}
//通过UUID生成唯一文件名
String filename = IdUtil.simpleUUID() + "." + suffix;
try {
//将文件保存指定目录
image.transferTo(new File(Constants.File.MARKDOWN_IMG_FOLDER.getPath() + filename));
} catch (Exception e) {
log.error("图片文件上传异常-------------->{}", e.getMessage());
return CommonResult.errorResponse("服务器异常:图片文件上传失败!", CommonResult.STATUS_ERROR);
}
return CommonResult.successResponse(MapUtil.builder()
.put("link", Constants.File.USER_FILE_HOST.getPath() + Constants.File.IMG_API.getPath() + filename)
.put("filePath",Constants.File.MARKDOWN_IMG_FOLDER.getPath() + filename).map(),
"上传图片成功!");
}
@RequestMapping(value = "/delete-md-img", method = RequestMethod.GET)
@RequiresAuthentication
@ResponseBody
@Transactional
@RequiresRoles(value = {"root", "admin"}, logical = Logical.OR)
public CommonResult uploadMDImg(@RequestParam("filePath") String filePath) {
boolean result = FileUtil.del(filePath);
if (result){
return CommonResult.successResponse(null,"删除成功");
}else{
return CommonResult.errorResponse("删除失败");
}
}
}

View File

@ -211,9 +211,10 @@ public class JudgeController {
// 将提交加入任务队列
if (judgeDto.getIsRemote()) { // 如果是远程oj判题
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, judgeDto.getPid(), judge.getCid() == 0);
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, judgeDto.getPid(),
judge.getCid() == 0, 1);
} else {
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, judge.getCid() == 0);
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, judge.getCid() == 0, 1);
}
return CommonResult.successResponse(judge, "数据提交成功!");
@ -223,7 +224,7 @@ public class JudgeController {
/**
* @MethodName resubmit
* @Params * @param null
* @Description 远程虚拟判题的提交失败用户点击按钮重新提交判题进入的方法
* @Description 调用判题服务器提交失败超过60s用户点击按钮重新提交判题进入的方法
* @Return
* @Since 2021/2/12
*/
@ -247,8 +248,14 @@ public class JudgeController {
judge.setStatus(Constants.Judge.STATUS_PENDING.getStatus());
judge.setErrorMessage(null);
judgeService.updateById(judge);
// 调用判题服务
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), problem.getId(), judgeToken, problem.getSource(), judge.getCid() == 0);
// 将提交加入任务队列
if (problem.getIsRemote()) { // 如果是远程oj判题
remoteJudgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken, problem.getProblemId(),
judge.getCid() == 0, 1);
} else {
judgeDispatcher.sendTask(judge.getSubmitId(), judge.getPid(), judgeToken,
judge.getCid() == 0, 1);
}
return CommonResult.successResponse(judge, "重新提交成功!");
}
@ -291,9 +298,10 @@ public class JudgeController {
Problem problem = problemService.getById(judge.getPid());
HashMap<String, Object> result = new HashMap<>();
// 只允许用户查看ce错误se错误信息提示
// 只允许用户查看ce错误,sf错误se错误信息提示
if (judge.getStatus().intValue() != Constants.Judge.STATUS_COMPILE_ERROR.getStatus() &&
judge.getStatus().intValue() != Constants.Judge.STATUS_SYSTEM_ERROR.getStatus()) {
judge.getStatus().intValue() != Constants.Judge.STATUS_SYSTEM_ERROR.getStatus() &&
judge.getStatus().intValue() != Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus()) {
judge.setErrorMessage("");
}
result.put("submission", judge);

View File

@ -36,7 +36,7 @@ public class HDUProblemStrategy extends ProblemStrategy {
info.setTitle(ReUtil.get("color:#1A5CC8\">([\\s\\S]*?)</h1>", html, 1).trim());
info.setTimeLimit(Integer.parseInt(ReUtil.get("(\\d*) MS", html, 1)));
info.setMemoryLimit(Integer.parseInt(ReUtil.get("/(\\d*) K", html, 1)) / 1024);
info.setDescription(ReUtil.get(">Problem Description</div>\\s+<.*?>(.*?)<br></div>", html, 1));
info.setDescription(ReUtil.get(">Problem Description</div>\\s+<.*?>(.*?)<br></div>", html, 1).replaceAll("src=\"../../", "src=\"" + HOST + "/"));
info.setInput(ReUtil.get(">Input</div>.*?<.*?>(.*?)<br></div>", html, 1));
info.setOutput(ReUtil.get(">Output</div>.*?<.*?>(.*?)<br></div>", html, 1));
StringBuilder sb = new StringBuilder("<input>");

View File

@ -21,13 +21,14 @@ public class RemoteJudgeDispatcher {
@Autowired
private JudgeServiceImpl judgeService;
public void sendTask(Long submitId, Long pid, String token, String remoteJudge, Boolean isContest) {
public void sendTask(Long submitId, Long pid, String token, String remoteJudge, Boolean isContest, Integer tryAgainNum) {
JSONObject task = new JSONObject();
task.set("submitId", submitId);
task.set("pid", pid);
task.set("remoteJudge", remoteJudge);
task.set("token", token);
task.set("isContest", isContest);
task.set("tryAgainNum", tryAgainNum);
try {
String account = (String) redisUtils.lrPop(Constants.Judge.getListNameByOJName(remoteJudge.split("-")[0]));
if (account != null) {

View File

@ -43,8 +43,9 @@ public class RemoteJudgeReceiver implements MessageListener {
Boolean isContest = task.getBool("isContest");
String username = task.getStr("username");
String password = task.getStr("password");
Integer tryAgainNum = task.getInt("tryAgainNum");
if (username == null || password == null) {
remoteJudgeDispatcher.sendTask(submitId, pid, token, remoteJudge, isContest);
remoteJudgeDispatcher.sendTask(submitId, pid, token, remoteJudge, isContest, tryAgainNum);
return;
}
Judge judge = judgeService.getById(submitId);
@ -54,6 +55,7 @@ public class RemoteJudgeReceiver implements MessageListener {
.setToken(token)
.setRemoteJudge(remoteJudge)
.setUsername(username)
.setPassword(password));
.setPassword(password)
.setTryAgainNum(tryAgainNum));
}
}

View File

@ -38,15 +38,13 @@ public class JudgeChooseRule extends AbstractLoadBalancerRule {
@Override
public Server choose(Object key) {
// 获取配置文件中所配置的集群名称
String clusterName = discoveryProperties.getClusterName();
// // 获取配置文件中所配置的集群名称
// String clusterName = discoveryProperties.getClusterName();
DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
// 需要请求的微服务名称
String serviceId = loadBalancer.getName();
// 获取该微服务的所有健康实例
List<Instance> instances = getInstances(serviceId);
System.out.println(instances);
// 进行匹配筛选的实例列表
List<Instance> metadataMatchInstances;
// 过滤出小于或等于规定最大并发判题任务数的服务实例

View File

@ -25,12 +25,13 @@ public class JudgeDispatcher {
@Autowired
private JudgeServiceImpl judgeService;
public void sendTask(Long submitId, Long pid, String token, Boolean isContest) {
public void sendTask(Long submitId, Long pid, String token, Boolean isContest,Integer tryAgainNum) {
JSONObject task = new JSONObject();
task.set("submitId", submitId);
task.set("pid", pid);
task.set("token", token);
task.set("isContest", isContest);
task.set("tryAgainNum", tryAgainNum);
try {
redisUtils.sendMessage(Constants.Judge.STATUS_JUDGE_WAITING.getName(), JSONUtil.toJsonStr(task));

View File

@ -18,8 +18,7 @@ import top.hcode.hoj.service.impl.JudgeServiceImpl;
/**
* @Author: Himit_ZH
* @Date: 2021/2/5 16:43
* @Description:
* 1. 判题信息的接受者调用判题服务对提交代码进行判断
* @Description: 1. 判题信息的接受者调用判题服务对提交代码进行判断
* 2. 若无空闲判题服务器会自动进入熔断机制重新将该判题信息发布到频道内
* 3. 再次接受到信息再次查询是否有空闲判题服务器若有则进行判题否则回到2
*/
@ -41,16 +40,18 @@ public class JudgeReceiver implements MessageListener {
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// //接收的topic
// String channel = stringRedisSerializer.deserialize(message.getChannel());
// 进行反序列化获取信息
String taskJson = (String) jackson2JsonRedisSerializer.deserialize(message.getBody());
JSONObject task = JSONUtil.parseObj(taskJson);
Long submitId = task.getLong("submitId");
String token = task.getStr("token");
Integer tryAgainNum = task.getInt("tryAgainNum");
Judge judge = judgeService.getById(submitId);
// 调用判题服务
toJudgeService.submitProblemJudge(new ToJudge().setJudge(judge).setToken(token).setRemoteJudge(null));
toJudgeService.submitProblemJudge(new ToJudge()
.setJudge(judge)
.setToken(token)
.setRemoteJudge(null)
.setTryAgainNum(tryAgainNum));
}
}

View File

@ -223,7 +223,7 @@ public class ProblemServiceImpl extends ServiceImpl<ProblemMapper, Problem> impl
checkProblemCase = remove && add;
}
} else {
} else if (problemDto.getSamples().size() > 0) {
int sumScore = 0;
// 新增加的case列表
List<ProblemCase> newProblemCaseList = new LinkedList<>();

View File

@ -141,8 +141,12 @@ public class Constants {
public enum File {
USER_FILE_HOST("http://localhost:9010"),
USER_AVATAR_FOLDER("D:\\avatar\\"),
USER_AVATAR_API("/public/img/"),
MARKDOWN_IMG_FOLDER("D:\\md\\"),
IMG_API("/public/img/"),
TESTCASE_BASE_FOLDER("D:\\zip\\"),

View File

@ -8,12 +8,6 @@ service-url:
hoj-judge-server: http://hoj-judge-server # 服务访问base_url
name: hoj-judge-server # 服务名
#激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
spring:
profiles: dev
# 配置文件上传限制
@ -79,9 +73,9 @@ shiro-redis:
ribbon:
# 指的是建立连接所用的时间,适用于网络状况正常的情况下,俩端连接所用的时间 单位是秒
ReadTimeout: 6000
ReadTimeout: 10000
# 指的是建立连接后从服务器读取到可用资源的时间
ConnectTimeout: 5000
ConnectTimeout: 30000
logging:
level:
@ -90,3 +84,20 @@ logging:
nacos:
client:
naming: error
# 开启Hystrix断路器
feign:
hystrix:
enabled: true
client:
config:
default:
connectTimeout: 15000
readTimeout: 60000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 60000

View File

@ -8,12 +8,6 @@ service-url:
hoj-judge-server: http://hoj-judge-server # 服务访问base_url
name: hoj-judge-server # 服务名
#激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
spring:
profiles: dev
# 配置文件上传限制
@ -79,9 +73,9 @@ shiro-redis:
ribbon:
# 指的是建立连接所用的时间,适用于网络状况正常的情况下,俩端连接所用的时间 单位是秒
ReadTimeout: 6000
ReadTimeout: 10000
# 指的是建立连接后从服务器读取到可用资源的时间
ConnectTimeout: 5000
ConnectTimeout: 30000
logging:
level:
@ -90,3 +84,20 @@ logging:
nacos:
client:
naming: error
# 开启Hystrix断路器
feign:
hystrix:
enabled: true
client:
config:
default:
connectTimeout: 15000
readTimeout: 60000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 60000

View File

@ -67,7 +67,7 @@ public class RemoteJudgeResultReceiver implements MessageListener {
// TODO 如果结果没出来重新放入队列并更新状态为Waiting
if (status.equals(Constants.Judge.STATUS_PENDING.getStatus())) {
try {
TimeUnit.SECONDS.sleep(2);
TimeUnit.SECONDS.sleep(1);
remoteJudgeResultDispatcher.sendTask(remoteJudge, submitId, uid, cid, pid, resultSubmitId);
} catch (Exception e) {
log.error("重新查询结果任务出错------{}", e.getMessage());

View File

@ -36,7 +36,7 @@ public class ToJudge implements Serializable {
@ApiModelProperty("远程判题所用密码")
private String password;
// @ApiModelProperty("重新尝试的次数,三次重新调用判题机依旧失败,直接判为系统错误")
// private Integer tryAgainNum;
@ApiModelProperty("重新尝试的次数,40次重新调用判题机依旧失败直接判为提交失败")
private Integer tryAgainNum;
}

File diff suppressed because it is too large Load Diff

View File

@ -17,12 +17,11 @@
"highlight.js": "^10.3.2",
"jquery": "^3.5.1",
"katex": "^0.12.0",
"mavon-editor": "^2.9.1",
"moment": "^2.29.1",
"muse-ui": "^3.0.2",
"nprogress": "^0.2.0",
"papaparse": "^5.3.0",
"tar-simditor": "^3.0.5",
"tar-simditor-markdown": "^1.2.3",
"vue": "^2.6.11",
"vue-avatar": "^2.3.3",
"vue-clipboard2": "^0.3.1",

View File

@ -80,6 +80,7 @@ export default {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
touch-action: none;
}
body {
background-color: #eee !important;
@ -264,4 +265,7 @@ a:hover {
.el-tag--dark {
border-color: #fff !important;
}
.v-note-wrapper .v-note-panel {
height: 460px !important;
}
</style>

View File

@ -0,0 +1,364 @@
/* Fira Code */
@font-face {
font-family: 'Fira Code', UbuntuMono, 'PingFang SC', 'Microsoft YaHei',
Helvetica, Arial, Menlo, Monaco, monospace, sans-serif;
font-style: normal;
src: local('Fira Code'), url('maize/FiraCode-Regular.ttf') format('ttf');
}
/* 全局属性 */
:root {
--select-text-bg-color: #e49123;
--bg-color: #fafafa; /*change background*/
--text-color: #333333; /*change text color*/
--md-char-color: #c7c5c5; /*change color of meta characetrs like `*` in markdown */
--meta-content-color: #5b808d; /*change color of meta contents like image text or link address in markdown */
--primary-color: #428bca; /* color of primary buttons */
--primary-btn-border-color: #285e8e;
--primary-btn-text-color: #fff;
--window-border: 1px solid #eee; /*border for sidebar, etc*/
--active-file-bg-color: #eee; /*background color if list item in file tree or file list*/
--active-file-text-color: inherit;
--active-file-border-color: #777;
--side-bar-bg-color: var(--bg-color); /*change background of sidebar*/
--item-hover-bg-color: rgba(
229,
229,
229,
0.59
); /*background of control items when hover, like menu in sidebar*/
--item-hover-text-color: inherit;
--monospace: monospace; /*monospace font for codes, fences*/
}
::selection {
background: rgba(33, 150, 243, 0.2);
}
::-moz-selection {
background: rgba(33, 150, 243, 0.2);
}
::-webkit-selection {
background: rgba(33, 150, 243, 0.2);
}
.maize-markdown-body {
font-size: 16px;
text-align: left;
letter-spacing: 0px;
font-family: 'Fira Code', '微软雅黑', 'PingFang SC', 'Microsoft YaHei',
sans-serif;
}
/*段落*/
.maize-markdown-body p {
font-size: 16px;
padding-top: 1rem;
padding-bottom: 1rem;
margin: 0;
line-height: 26px;
color: black;
}
/* todo item 对齐 */
.maize-markdown-body input[type='checkbox'] {
margin-top: calc(1em - 2px);
margin-right: 5px;
left: -3px;
}
/*标题*/
.maize-markdown-body h1,
.maize-markdown-body h2,
.maize-markdown-body h3,
.maize-markdown-body h4,
.maize-markdown-body h5,
.maize-markdown-body h6 {
margin: 0.72em 0;
padding: 0px;
font-weight: bold;
color: black;
}
.maize-markdown-body h1 {
margin: 1.2em 0;
font-size: 2rem;
/* text-align: center; */
}
.maize-markdown-body h2::before {
content: '✔';
font-weight: bold;
color: #409eff;
margin-right: 4px;
}
.maize-markdown-body h2 {
font-size: 1.7rem;
margin: 1em 0;
}
.maize-markdown-body h2 span {
font-weight: bold;
color: #333;
padding: 3px 10px 1px;
}
.maize-markdown-body h3 {
font-size: 1.5rem;
}
.maize-markdown-body h4 {
font-size: 1.25rem;
}
.maize-markdown-body h5 {
font-size: 1rem;
}
.maize-markdown-body h6 {
font-size: 1rem;
}
/* 列表 */
.maize-markdown-body ul,
.maize-markdown-body ol {
margin-top: 8px;
margin-bottom: 8px;
padding-left: 40px;
color: black;
}
.maize-markdown-body ul {
list-style-type: disc;
}
.maize-markdown-body ul ul {
list-style-type: square;
}
.maize-markdown-body ol {
list-style-type: decimal;
}
/* 列表内容 */
.maize-markdown-body li section {
font-size: 15px;
}
/* 字体加粗 */
.maize-markdown-body strong::before {
content: '「';
}
.maize-markdown-body strong {
color: #2196f3;
font-weight: bold;
}
.maize-markdown-body strong::after {
content: '」';
}
/*引用*/
.maize-markdown-body blockquote {
margin-bottom: 16px;
margin-top: 16px;
padding: 10px 10px 10px 20px;
font-size: 0.9em;
background: #e8f4fd;
border-left: 3px solid #2196f3;
color: #6a737d;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.maize-markdown-body blockquote p {
line-height: 26px;
}
/* 超链接 */
.maize-markdown-body a {
text-decoration: none;
font-weight: bold;
color: #2196f3;
border-bottom: 1px solid #2196f3;
}
.maize-markdown-body a:hover {
color: #ff5722 !important;
}
/* 分割线 */
.maize-markdown-body hr {
height: 1px;
padding: 0;
border: none;
text-align: center;
background-image: linear-gradient(
to right,
rgba(231, 93, 109, 0.3),
rgba(255, 159, 150, 0.75),
rgba(255, 216, 181, 0.3)
);
}
/* 行内代码 */
.maize-markdown-body p code,
.maize-markdown-body span code,
.maize-markdown-body li code {
word-wrap: break-word;
padding: 2px 4px;
border-radius: 4px;
margin: 0 2px;
word-break: break-all;
}
/* 文章插图 */
.maize-markdown-body img {
display: block;
margin: 0 auto;
margin-top: 1rem;
margin-bottom: 1rem;
width: 100%;
max-width: 100%;
border-radius: 5px;
box-shadow: 0px 4px 12px #84a1a8;
border: 0px;
}
.maize-markdown-body table tr {
border: 0;
border-top: 1px solid #ccc;
background-color: white;
}
.maize-markdown-body table tr:nth-child(2n) {
background-color: #f8f8f8;
}
.maize-markdown-body table tr th,
.maize-markdown-body table tr td {
font-size: 16px;
line-height: 1.5;
border: 1px solid #ccc;
padding: 5px 10px;
text-align: left;
}
.maize-markdown-body table tr th {
font-weight: bold;
background-color: #f0f0f0;
}
/* 上角标 */
.maize-markdown-body .md-footnote {
font-weight: bold;
color: #e49123;
}
.maize-markdown-body .md-footnote > .md-text:before {
content: '[';
}
.maize-markdown-body .md-footnote > .md-text:after {
content: ']';
}
/* 下角标 */
.maize-markdown-body .md-def-name {
padding-right: 1.8ch;
}
.maize-markdown-body .md-def-name:before {
content: '[';
color: #000;
}
.maize-markdown-body .md-def-name:after {
color: #000;
}
/* 代码块主题 */
code,
.md-fences {
padding: 0.5em;
/* border: 1px solid #ccc; */
padding: 0.1em;
border-radius: 5px;
margin-left: 0.2em;
margin-right: 0.2em;
font-family: 'Fira Code', '微软雅黑', 'PingFang SC', 'Microsoft YaHei',
sans-serif;
}
.md-fences {
margin: 0 0 20px;
background-color: #292b35;
color: rgb(236, 236, 236);
/* font-size: 1em; */
padding: 0.3em 1em;
padding-top: 0.4em;
box-shadow: 0px 4px 9px grey;
}
.CodeMirror div.CodeMirror-cursor {
margin-left: 4.6px;
border-left: 1px solid #7ba0f0;
z-index: 5;
}
.cm-s-inner div.CodeMirror-selected {
background: rgba(113, 124, 180, 0.2);
}
.CodeMirror-code {
margin-left: 5px;
}
.cm-s-inner .cm-keyword {
color: #c792ea;
font-weight: 450;
}
.cm-s-inner .cm-operator {
color: #89ddff;
}
.cm-s-inner .cm-variable-2 {
color: #eeffff;
}
.cm-s-inner .cm-variable-3,
.cm-s-inner .cm-type {
color: #f07178;
}
.cm-s-inner .cm-builtin {
color: #ffcb6b;
}
.cm-s-inner .cm-atom {
color: #f78c6c;
}
.cm-s-inner .cm-number {
color: #ff5370;
}
.cm-s-inner .cm-def {
color: #82aaff;
}
.cm-s-inner .cm-string {
color: #c3e88d;
}
.cm-s-inner .cm-string-2 {
color: #f07178;
}
.cm-s-inner .cm-comment {
color: #676e95;
}
.cm-s-inner .cm-variable {
color: #f07178;
}
.cm-s-inner .cm-tag {
color: #ff5370;
}
.cm-s-inner .cm-meta {
color: #ffcb6b;
}
.cm-s-inner .cm-attribute {
color: #c792ea;
}
.cm-s-inner .cm-property {
color: #c792ea;
}
.cm-s-inner .cm-qualifier {
color: #decb6b;
}
.cm-s-inner .cm-variable-3,
.cm-s-inner .cm-type {
color: #decb6b;
}
.cm-s-inner .cm-error {
color: rgba(255, 255, 255, 1);
background-color: #e9405c;
}
/*流程图样式*/
pre[lang='sequence'],
pre[lang='flow'],
pre[lang='mermaid'] {
background: #ffffff;
color: #333333;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,73 @@
<template>
<div class="mavonEditor">
<mavon-editor
ref="md"
@imgAdd="$imgAdd"
@imgDel="$imgDel"
v-model="currentValue"
codeStyle="arduino-light"
></mavon-editor>
</div>
</template>
<script>
import myMessage from '@/common/message';
export default {
name: 'Editor',
props: {
value: {
type: String,
default: '',
},
},
data() {
return {
currentValue: this.value,
img_file: {},
};
},
methods: {
// md
$imgAdd(pos, $file) {
var formdata = new FormData();
formdata.append('image', $file);
//
this.$http({
url: '/file/upload-md-img',
method: 'post',
data: formdata,
headers: { 'Content-Type': 'multipart/form-data' },
}).then((res) => {
this.$refs.md.$img2Url(pos, res.data.data.link);
this.img_file[res.data.data.link] = res.data.data.filePath;
});
},
$imgDel(pos) {
//
this.$http({
url: '/file/delete-md-img',
method: 'get',
params: {
filePath: this.img_file[pos[0]],
},
});
},
},
watch: {
value(val) {
if (this.currentValue !== val) {
this.currentValue = val;
}
},
currentValue(newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit('update:value', newVal);
}
},
},
};
</script>
<style scoped>
.mavonEditor {
height: 500px;
}
</style>

View File

@ -1,74 +0,0 @@
<template>
<textarea ref="editor"></textarea>
</template>
<script>
import Simditor from 'tar-simditor' //
import 'tar-simditor/styles/simditor.css'
import 'tar-simditor-markdown'
import 'tar-simditor-markdown/styles/simditor-markdown.css'
import './simditor-file-upload'
export default {
name: 'Simditor',
props: {
toolbar: {
type: Array,
default: () => ['title', 'bold', 'italic', 'underline', 'fontScale', 'color', 'ol', 'ul', '|', 'link', 'image', 'uploadfile', 'hr', '|', 'indent', 'outdent', 'alignment', '|', 'markdown']
},
value: {
type: String,
default: ''
}
},
data () {
return {
editor: null,
currentValue: this.value
}
},
mounted () {
this.editor = new Simditor({
textarea: this.$refs.editor,
toolbar: this.toolbar,
pasteImage: true,
markdown: false,
upload: {
url: '/api/admin/upload_image/',
params: null,
fileKey: 'image',//
connectionCount: 3,
leaveConfirm: '正在上传文件'
},
allowedStyles: {
span: ['color']
}
})
this.editor.on('valuechanged', (e, src) => {
this.currentValue = this.editor.getValue()
})
this.editor.on('decorate', (e, src) => {
this.currentValue = this.editor.getValue()
})
this.editor.setValue(this.value)
},
watch: {
'value' (val) {
if (this.currentValue !== val) {
this.currentValue = val
this.editor.setValue(val)
}
},
'currentValue' (newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit('change', newVal)
this.$emit('input', newVal)
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,85 +0,0 @@
/* eslint-disable */
import Simditor from 'tar-simditor'
import * as $ from 'jquery'
var UploadFile,
__hasProp = {}.hasOwnProperty,
__extends = function (child, parent) {
for (var key in parent) {
if (__hasProp.call(parent, key)) child[key] = parent[key];
}
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;
return child;
},
__slice = [].slice;
UploadFile = (function (_super) {
__extends(UploadFile, _super);
UploadFile.i18n = {
'zh-CN': {
uploadfile: '上传文件'
},
'en-US': {
uploadfile: 'upload file'
}
};
UploadFile.prototype.name = 'uploadfile';
UploadFile.prototype.icon = 'upload';
function UploadFile() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
UploadFile.__super__.constructor.apply(this, args);
this._initUpload();
}
UploadFile.prototype._initUpload = function () {
this.input = $('<input />', {
type: 'file',
style: 'position:absolute;top:0;right:0;height:100%;width:100%;opacity:0;filter:alpha(opacity=0);cursor:pointer;'
}).prependTo(this.el)
var _this = this;
this.el.on('click mousedown', 'input[type=file]', function (e) {
return e.stopPropagation();
}).on('change', 'input[type=file]', function (e) {
var formData = new FormData();
formData.append('file', this.files[0]);
$.ajax({
url: '/api/admin/upload_file',
type: 'POST',
cache: false,
data: formData,
processData: false,
contentType: false
}).done(function (res) {
if (!res.success) {
alert("upload file failed")
} else {
let link = '<a target="_blank" className="simditor-attach-link" href="' + res.file_path + '">' + res.file_name + '</a>'
_this.editor.setValue(_this.editor.getValue() + link)
}
}).fail(function (res) {
alert("upload file failed")
});
});
}
return UploadFile;
})(Simditor.Button);
Simditor.Toolbar.addButton(UploadFile);

View File

@ -13,7 +13,12 @@
:loading="btnLoading"
>Refresh</el-button
>
<el-button v-else type="primary" icon="el-icon-back" @click="goBack" size="small"
<el-button
v-else
type="primary"
icon="el-icon-back"
@click="goBack"
size="small"
>Back</el-button
>
</span>
@ -38,16 +43,12 @@
<div class="info">
<span class="date">
<i class="el-icon-edit"></i>
{{ announcement.gmtCreate | localtime }}
</span>
<span class="creator">
<i class="el-icon-user"></i>
{{ announcement.username }}
</span>
</div>
</div>
@ -66,9 +67,10 @@
<template v-else>
<div
v-katex
v-highlight
v-html="announcement.content"
key="content"
class="content-container markdown-body"
class="content-container maize-markdown-body"
></div>
</template>
</transition-group>
@ -76,11 +78,10 @@
</template>
<script>
import api from "@/common/api";
import Pagination from "@/components/oj/common/Pagination";
import api from '@/common/api';
import Pagination from '@/components/oj/common/Pagination';
export default {
name: "Announcement",
name: 'Announcement',
components: {
Pagination,
},
@ -90,7 +91,7 @@ export default {
total: 0,
btnLoading: false,
announcements: [],
announcement: "",
announcement: '',
listVisible: true,
};
},
@ -120,7 +121,13 @@ export default {
},
getContestAnnouncementList(page = 1) {
this.btnLoading = true;
api.getContestAnnouncementList(page, this.limit,this.$route.params.contestID).then(
api
.getContestAnnouncementList(
page,
this.limit,
this.$route.params.contestID
)
.then(
(res) => {
this.btnLoading = false;
this.announcements = res.data.data.records;
@ -133,17 +140,18 @@ export default {
},
goAnnouncement(announcement) {
this.announcement = announcement;
this.announcement.content = this.$markDown.render(announcement.content);
this.listVisible = false;
},
goBack() {
this.listVisible = true;
this.announcement = "";
this.announcement = '';
},
},
computed: {
title() {
if (this.listVisible) {
return this.isContest ? "Contest Announcements" : "Announcements";
return this.isContest ? 'Contest Announcements' : 'Announcements';
} else {
return this.announcement.title;
}
@ -168,7 +176,7 @@ export default {
margin-top: 10px;
font-size: 16px;
border: 1px solid rgba(187, 187, 187, 0.5);
border-left: 2px solid #409EFF;
border-left: 2px solid #409eff;
}
/* .announcements-container li:last-child {
border-bottom: none;

View File

@ -43,6 +43,12 @@ import 'echarts/lib/component/markPoint'
import VueParticles from 'vue-particles'
import SlideVerify from 'vue-monoplasty-slide-verify'
// markdown编辑器
import mavonEditor from 'mavon-editor' //引入markdown编辑器
import 'mavon-editor/dist/css/index.css'
// 前端所用markdown样式
import '@/assets/css/maize.css'
Vue.use(mavonEditor)
Object.keys(filters).forEach(key => { // 注册全局过滤器
Vue.filter(key, filters[key])
@ -61,6 +67,9 @@ Vue.use(SlideVerify) // 滑动验证码组件
Vue.component('ECharts', ECharts)
Vue.prototype.$axios = axios
Vue.prototype.$markDown = mavonEditor.markdownIt
Vue.config.productionTip = false
new Vue({
router,

View File

@ -10,12 +10,15 @@
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="Contest Title" required>
<el-input v-model="contest.title" placeholder="Enter the Contest Title"></el-input>
<el-input
v-model="contest.title"
placeholder="Enter the Contest Title"
></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="Contest Description" required>
<Simditor v-model="contest.description"></Simditor>
<Editor :value.sync="contest.description"></Editor>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
@ -24,7 +27,8 @@
v-model="contest.startTime"
@change="changeDuration"
type="datetime"
placeholder="Enter the Contest Start Time">
placeholder="Enter the Contest Start Time"
>
</el-date-picker>
</el-form-item>
</el-col>
@ -34,21 +38,33 @@
v-model="contest.endTime"
@change="changeDuration"
type="datetime"
placeholder="Enter the Contest End Time">
placeholder="Enter the Contest End Time"
>
</el-date-picker>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest Duration" required>
<el-input v-model="durationText" disabled>
</el-input>
<el-input v-model="durationText" disabled> </el-input>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest Type">
<el-radio class="radio" v-model="contest.type" :label="0" :disabled="disableRuleType">ACM</el-radio>
<el-radio class="radio" v-model="contest.type" :label="1" :disabled="disableRuleType">OI</el-radio>
<el-radio
class="radio"
v-model="contest.type"
:label="0"
:disabled="disableRuleType"
>ACM</el-radio
>
<el-radio
class="radio"
v-model="contest.type"
:label="1"
:disabled="disableRuleType"
>OI</el-radio
>
</el-form-item>
</el-col>
@ -57,7 +73,8 @@
<el-switch
v-model="contest.sealRank"
active-color="#13ce66"
inactive-color="#ff4949">
inactive-color="#ff4949"
>
</el-switch>
</el-form-item>
</el-col>
@ -67,16 +84,29 @@
<el-switch
v-model="contest.sealRank"
active-color="#13ce66"
inactive-color="">
inactive-color=""
>
</el-switch>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Seal Rank Time" :required="contest.sealRank" v-show="contest.sealRank">
<el-form-item
label="Seal Rank Time"
:required="contest.sealRank"
v-show="contest.sealRank"
>
<el-select v-model="seal_rank_time">
<el-option label="比赛结束前半小时" :value="0" :disabled="contest.duration<1800"></el-option>
<el-option label="比赛结束前一小时" :value="1" :disabled="contest.duration<3600"></el-option>
<el-option
label="比赛结束前半小时"
:value="0"
:disabled="contest.duration < 1800"
></el-option>
<el-option
label="比赛结束前一小时"
:value="1"
:disabled="contest.duration < 3600"
></el-option>
<el-option label="比赛全程时间" :value="2"></el-option>
</el-select>
</el-form-item>
@ -92,8 +122,15 @@
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest Password" v-show="contest.auth!=0" :required="contest.auth!=0">
<el-input v-model="contest.pwd" placeholder="Contest Password"></el-input>
<el-form-item
label="Contest Password"
v-show="contest.auth != 0"
:required="contest.auth != 0"
>
<el-input
v-model="contest.pwd"
placeholder="Contest Password"
></el-input>
</el-form-item>
</el-col>
<!-- <el-col :span="24">
@ -119,16 +156,16 @@
</template>
<script>
import api from '@/common/api'
import Simditor from '@/components/admin/Simditor.vue'
import time from '@/common/time'
import moment from 'moment'
import { mapGetters } from "vuex";
import myMessage from '@/common/message'
import api from '@/common/api';
import Editor from '@/components/admin/Editor.vue';
import time from '@/common/time';
import moment from 'moment';
import { mapGetters } from 'vuex';
import myMessage from '@/common/message';
export default {
name: 'CreateContest',
components: {
Simditor
Editor,
},
data() {
return {
@ -150,36 +187,38 @@
// allowed_ip_ranges: [{
// value: ''
// }]
}
}
},
};
},
mounted() {
if (this.$route.name === 'admin-edit-contest') {
this.title = 'Edit Contest'
this.disableRuleType = true
this.title = 'Edit Contest';
this.disableRuleType = true;
this.getContestByCid();
}
},
watch: {
$route() {
if (this.$route.name === 'admin-edit-contest') {
this.title = 'Edit Contest'
this.disableRuleType = true
this.title = 'Edit Contest';
this.disableRuleType = true;
this.getContestByCid();
} else {
this.title = 'Create Contest'
this.disableRuleType = false
this.contest = []
}
this.title = 'Create Contest';
this.disableRuleType = false;
this.contest = [];
}
},
},
computed: {
...mapGetters(["userInfo"]),
...mapGetters(['userInfo']),
},
methods: {
getContestByCid() {
api.admin_getContest(this.$route.params.contestId).then(res => {
let data = res.data.data
api
.admin_getContest(this.$route.params.contestId)
.then((res) => {
let data = res.data.data;
// let ranges = []
// for (let v of data.allowed_ip_ranges) {
// ranges.push({value: v})
@ -188,13 +227,17 @@
// ranges.push({value: ''})
// }
// data.allowed_ip_ranges = ranges
this.contest = data
this.changeDuration()
this.contest = data;
this.changeDuration();
//
let halfHour = moment(this.contest.endTime).subtract(1800,'seconds').toString()
let oneHour = moment(this.contest.endTime).subtract(3600,'seconds').toString()
let allHour = moment(this.contest.startTime).toString()
let sealRankTime = moment(this.contest.sealRankTime).toString()
let halfHour = moment(this.contest.endTime)
.subtract(1800, 'seconds')
.toString();
let oneHour = moment(this.contest.endTime)
.subtract(3600, 'seconds')
.toString();
let allHour = moment(this.contest.startTime).toString();
let sealRankTime = moment(this.contest.sealRankTime).toString();
switch (sealRankTime) {
case halfHour:
this.seal_rank_time = 0;
@ -206,24 +249,33 @@
this.seal_rank_time = 2;
break;
}
}).catch(() => {
})
.catch(() => {});
},
saveContest() {
let funcName = this.$route.name === 'admin-edit-contest' ? 'admin_editContest' : 'admin_createContest'
let funcName =
this.$route.name === 'admin-edit-contest'
? 'admin_editContest'
: 'admin_createContest';
switch (this.seal_rank_time) {
case 0: //
this.contest.sealRankTime = moment(this.contest.endTime).subtract(1800,'seconds');
this.contest.sealRankTime = moment(this.contest.endTime).subtract(
1800,
'seconds'
);
break;
case 1: //
this.contest.sealRankTime = moment(this.contest.endTime).subtract(3600,'seconds');
this.contest.sealRankTime = moment(this.contest.endTime).subtract(
3600,
'seconds'
);
break;
case 2: //
this.contest.sealRankTime = moment(this.contest.startTime);
}
let data = Object.assign({}, this.contest)
let data = Object.assign({}, this.contest);
// let ranges = []
// for (let v of data.allowed_ip_ranges) {
// if (v.value !== '') {
@ -232,22 +284,26 @@
// }
// data.allowed_ip_ranges = ranges
if (funcName === 'admin_createContest') {
data['uid'] = this.userInfo.uid
data['author'] = this.userInfo.username
data['uid'] = this.userInfo.uid;
data['author'] = this.userInfo.username;
}
api[funcName](data).then((res) => {
myMessage.success(res.data.msg)
this.$router.push({name: 'admin-contest-list', query: {refresh: 'true'}})
}).catch(() => {
api[funcName](data)
.then((res) => {
myMessage.success(res.data.msg);
this.$router.push({
name: 'admin-contest-list',
query: { refresh: 'true' },
});
})
.catch(() => {});
},
changeDuration() {
let start = this.contest.startTime;
let end = this.contest.endTime
let end = this.contest.endTime;
let durationMS = time.durationMs(start, end);
if (durationMS < 0) {
this.durationText = '比赛起始时间不应该晚于结束时间!'
this.durationText = '比赛起始时间不应该晚于结束时间!';
this.contest.duration = 0;
return;
}
@ -266,5 +322,5 @@
// }
// }
},
}
};
</script>

View File

@ -5,7 +5,13 @@
<span class="panel-title home-title">Announcement</span>
</div>
<div class="create">
<el-button type="primary" size="small" @click="openAnnouncementDialog(null)" icon="el-icon-plus">Create</el-button>
<el-button
type="primary"
size="small"
@click="openAnnouncementDialog(null)"
icon="el-icon-plus"
>Create</el-button
>
</div>
<div class="list">
<vxe-table
@ -15,20 +21,15 @@
auto-resize
stripe
>
<vxe-table-column
min-width="50"
field="id"
title="ID">
<vxe-table-column min-width="50" field="id" title="ID">
</vxe-table-column>
<vxe-table-column
min-width="150"
field="title"
title="Title">
<vxe-table-column min-width="150" field="title" title="Title">
</vxe-table-column>
<vxe-table-column
min-width="150"
field="gmtCreate"
title="Create Time">
title="Create Time"
>
<template v-slot="{ row }">
{{ row.gmtCreate | localtime }}
</template>
@ -36,39 +37,54 @@
<vxe-table-column
min-width="150"
field="gmtModified"
title="Last Update Time">
title="Last Update Time"
>
<template v-slot="{ row }">
{{ row.gmtModified | localtime }}
</template>
</vxe-table-column>
<vxe-table-column
min-width="150"
field="username"
title="Author">
<vxe-table-column min-width="150" field="username" title="Author">
</vxe-table-column>
<vxe-table-column
min-width="100"
field="status"
title="Visible">
<vxe-table-column min-width="100" field="status" title="Visible">
<template v-slot="{ row }">
<el-switch v-model="row.status"
<el-switch
v-model="row.status"
active-text=""
inactive-text=""
:active-value="0"
:inactive-value="1"
@change="handleVisibleSwitch(row)">
@change="handleVisibleSwitch(row)"
>
</el-switch>
</template>
</vxe-table-column>
<vxe-table-column
title="Option"
min-width="150">
<vxe-table-column title="Option" min-width="150">
<template v-slot="row">
<el-tooltip class="item" effect="dark" content="编辑公告" placement="top">
<el-button icon="el-icon-edit-outline" @click.native="openAnnouncementDialog(row)" size="mini" type="primary"></el-button>
<el-tooltip
class="item"
effect="dark"
content="编辑公告"
placement="top"
>
<el-button
icon="el-icon-edit-outline"
@click.native="openAnnouncementDialog(row.row)"
size="mini"
type="primary"
></el-button>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="删除公告" placement="top">
<el-button icon="el-icon-delete-solid" @click.native="deleteAnnouncement(row.row.id)" size="mini" type="danger"></el-button>
<el-tooltip
class="item"
effect="dark"
content="删除公告"
placement="top"
>
<el-button
icon="el-icon-delete-solid"
@click.native="deleteAnnouncement(row.row.id)"
size="mini"
type="danger"
></el-button>
</el-tooltip>
</template>
</vxe-table-column>
@ -81,24 +97,31 @@
layout="prev, pager, next"
@current-change="currentChange"
:page-size="pageSize"
:total="total">
:total="total"
>
</el-pagination>
</div>
</div>
</el-card>
<!--编辑公告对话框-->
<el-dialog :title="announcementDialogTitle" :visible.sync="showEditAnnouncementDialog" :fullscreen="true"
@open="onOpenEditDialog">
<el-dialog
:title="announcementDialogTitle"
:visible.sync="showEditAnnouncementDialog"
:fullscreen="true"
@open="onOpenEditDialog"
>
<el-form label-position="top" :model="announcement">
<el-form-item label="公告标题" required>
<el-input
v-model="announcement.title"
placeholder="请输入公告标题" class="title-input">
placeholder="请输入公告标题"
class="title-input"
>
</el-input>
</el-form-item>
<el-form-item label="公告内容" required>
<Simditor v-model="announcement.content"></Simditor>
<Editor :value.sync="announcement.content"></Editor>
</el-form-item>
<div class="visible-box">
<span>是否显示</span>
@ -107,27 +130,34 @@
:active-value="0"
:inactive-value="1"
active-text=""
inactive-text="">
inactive-text=""
>
</el-switch>
</div>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="danger" @click.native="showEditAnnouncementDialog = false">Cancel</el-button>
<el-button type="primary" @click.native="submitAnnouncement">Save</el-button>
<el-button
type="danger"
@click.native="showEditAnnouncementDialog = false"
>Cancel</el-button
>
<el-button type="primary" @click.native="submitAnnouncement"
>Save</el-button
>
</span>
</el-dialog>
</div>
</template>
<script>
import Simditor from '@/components/admin/Simditor.vue'
import api from '@/common/api'
import myMessage from '@/common/message'
import { mapGetters } from "vuex";
import Editor from '@/components/admin/Editor.vue';
import api from '@/common/api';
import myMessage from '@/common/message';
import { mapGetters } from 'vuex';
export default {
name: 'announcement',
components: {
Simditor
Editor,
},
data() {
return {
@ -156,50 +186,56 @@
// loading
loading: false,
//
currentPage: 0
}
currentPage: 0,
};
},
mounted() {
this.init()
this.init();
},
methods: {
init() {
this.contestID = this.$route.params.contestId
this.contestID = this.$route.params.contestId;
if (this.contestID) {
this.getContestAnnouncementList(1)
this.getContestAnnouncementList(1);
} else {
this.getAnnouncementList(1)
this.getAnnouncementList(1);
}
},
//
currentChange(page) {
this.currentPage = page
this.currentPage = page;
if (this.contestID) {
this.getContestAnnouncementList(page)
this.getContestAnnouncementList(page);
} else {
this.getAnnouncementList(page)
this.getAnnouncementList(page);
}
},
getAnnouncementList(page) {
this.loading = true
api.admin_getAnnouncementList(page, this.pageSize).then(res => {
this.loading = false
this.total = res.data.data.total
this.announcementList = res.data.data.records
}, res => {
this.loading = false
})
this.loading = true;
api.admin_getAnnouncementList(page, this.pageSize).then(
(res) => {
this.loading = false;
this.total = res.data.data.total;
this.announcementList = res.data.data.records;
},
(res) => {
this.loading = false;
}
);
},
getContestAnnouncementList(page) {
this.loading = true
api.admin_getContestAnnouncementList(this.contestID,page,this.pageSize).then(res => {
this.loading = false
this.total = res.data.data.total
this.announcementList = res.data.data.records
}).catch(() => {
this.loading = false
this.loading = true;
api
.admin_getContestAnnouncementList(this.contestID, page, this.pageSize)
.then((res) => {
this.loading = false;
this.total = res.data.data.total;
this.announcementList = res.data.data.records;
})
.catch(() => {
this.loading = false;
});
},
//
onOpenEditDialog() {
@ -207,38 +243,46 @@
// bug
setTimeout(() => {
if (document.createEvent) {
let event = document.createEvent('HTMLEvents')
event.initEvent('resize', true, true)
window.dispatchEvent(event)
let event = document.createEvent('HTMLEvents');
event.initEvent('resize', true, true);
window.dispatchEvent(event);
} else if (document.createEventObject) {
window.fireEvent('onresize')
window.fireEvent('onresize');
}
}, 0)
}, 0);
},
//
// MouseEvent
submitAnnouncement(data = undefined) {
let funcName = ''
let funcName = '';
if (!data.id) {
data = this.announcement
data = this.announcement;
}
let requestData;
if (this.contestID) {
let announcement = {
announcement: data,
cid:this.contestID
}
requestData = announcement
funcName = this.mode === 'edit' ? 'admin_updateContestAnnouncement' : 'admin_createContestAnnouncement'
cid: this.contestID,
};
requestData = announcement;
funcName =
this.mode === 'edit'
? 'admin_updateContestAnnouncement'
: 'admin_createContestAnnouncement';
} else {
funcName = this.mode === 'edit' ? 'admin_updateAnnouncement' : 'admin_createAnnouncement'
requestData = data
funcName =
this.mode === 'edit'
? 'admin_updateAnnouncement'
: 'admin_createAnnouncement';
requestData = data;
}
api[funcName](requestData).then(res => {
this.showEditAnnouncementDialog = false
api[funcName](requestData)
.then((res) => {
this.showEditAnnouncementDialog = false;
myMessage.success(res.data.msg);
this.init()
}).catch()
this.init();
})
.catch();
},
//
@ -246,58 +290,62 @@
this.$confirm('你确定要删除该公告?', 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
type: 'warning',
})
.then(() => {
// then
this.loading = true
let funcName = this.contestID ? 'admin_deleteContestAnnouncement' : 'admin_deleteAnnouncement'
api[funcName](announcementId).then(res => {
this.loading = true
this.loading = true;
let funcName = this.contestID
? 'admin_deleteContestAnnouncement'
: 'admin_deleteAnnouncement';
api[funcName](announcementId).then((res) => {
this.loading = true;
myMessage.success(res.data.msg);
this.init()
this.init();
});
})
}).catch(() => {
.catch(() => {
// catch
this.loading = false
})
this.loading = false;
});
},
openAnnouncementDialog(row) {
this.showEditAnnouncementDialog = true
this.showEditAnnouncementDialog = true;
if (row !== null) {
this.announcementDialogTitle = 'Edit Announcement'
this.announcement = Object.assign({},row.data[0])
this.mode = 'edit'
this.announcementDialogTitle = 'Edit Announcement';
this.announcement = Object.assign({}, row);
this.mode = 'edit';
} else {
this.announcementDialogTitle = 'Create Announcement'
this.announcement.title = ''
this.announcement.status = 0
this.announcement.content = ''
this.announcement.uid = this.userInfo.uid
this.announcement.username = this.userInfo.username
this.mode = 'create'
this.announcementDialogTitle = 'Create Announcement';
this.announcement.title = '';
this.announcement.status = 0;
this.announcement.content = '';
this.announcement.uid = this.userInfo.uid;
this.announcement.username = this.userInfo.username;
this.mode = 'create';
}
},
handleVisibleSwitch(row) {
this.mode = 'edit'
this.mode = 'edit';
this.submitAnnouncement({
id: row.id,
title: row.title,
content: row.content,
status: row.status,
uid:row.uid
})
}
uid: row.uid,
});
},
},
watch: {
$route() {
this.init()
}
this.init();
},
},
computed: {
...mapGetters(["userInfo"]),
}
}
...mapGetters(['userInfo']),
},
};
</script>
<style scoped>

View File

@ -59,7 +59,7 @@
<el-row :gutter="20">
<el-col :span="24">
<el-form-item prop="description" label="Description" required>
<Simditor v-model="problem.description"></Simditor>
<Editor :value.sync="problem.description"></Editor>
</el-form-item>
</el-col>
</el-row>
@ -107,7 +107,7 @@
label="Input Description"
required
>
<Simditor v-model="problem.input"></Simditor>
<Editor :value.sync="problem.input"></Editor>
</el-form-item>
</el-col>
<el-col :span="24">
@ -116,7 +116,7 @@
label="Output Description"
required
>
<Simditor v-model="problem.output"></Simditor>
<Editor :value.sync="problem.output"></Editor>
</el-form-item>
</el-col>
</el-row>
@ -322,7 +322,7 @@
</template>
<el-form-item style="margin-top: 20px" label="Hint">
<Simditor v-model="problem.hint"></Simditor>
<Editor :value.sync="problem.hint"></Editor>
</el-form-item>
<el-row :gutter="20">
@ -350,6 +350,7 @@
<el-switch
v-model="isUploadTestCase"
@change="changeSampleUploadMethod"
active-text="Use Upload File"
inactive-text="Use Manual Input"
style="margin: 10px 0"
@ -506,7 +507,7 @@
</template>
<script>
import Simditor from '@/components/admin/Simditor';
import Editor from '@/components/admin/Editor';
import Accordion from '@/components/admin/Accordion';
import CodeMirror from '@/components/admin/CodeMirror';
import utils from '@/common/utils';
@ -517,9 +518,9 @@ import { OJ_NAME } from '@/common/constants';
export default {
name: 'Problem',
components: {
Simditor,
Accordion,
CodeMirror,
Editor,
},
data() {
return {
@ -727,9 +728,11 @@ export default {
this.problemLanguages.push(Languages[i].name);
}
});
if (!this.isUploadTestCase) {
api.admin_getProblemCases(this.pid).then((res) => {
this.problemSamples = res.data.data;
});
}
api.admin_getProblemTags(this.pid).then((res) => {
this.problemTags = res.data.data;
});
@ -740,6 +743,13 @@ export default {
}
}
},
changeSampleUploadMethod() {
if (!this.isUploadTestCase && this.problemSamples.length == 0) {
api.admin_getProblemCases(this.pid).then((res) => {
this.problemSamples = res.data.data;
});
}
},
switchSpj() {
if (this.testCaseUploaded) {
this.$confirm(
@ -773,6 +783,9 @@ export default {
})
.catch(() => {});
},
changeContent(newVal) {
this.announcement.content = newVal;
},
resetTestCase() {
this.testCaseUploaded = false;
this.problem.testCaseScore = [];

View File

@ -53,7 +53,10 @@
</el-button>
</div>
<div class="contest-description">
<blockquote v-html="contest.description"></blockquote>
<blockquote
v-html="contest.description"
class="maize-markdown-body"
></blockquote>
</div>
</div>
</el-carousel-item>

View File

@ -97,7 +97,7 @@
<el-tab-pane name="ContestDetails" lazy>
<span slot="label"><i class="el-icon-s-home"></i>&nbsp;Overview</span>
<el-card class="box-card">
<div v-html="contest.description" class="markdown-body"></div>
<div v-html="contest.description" class="maize-markdown-body"></div>
</el-card>
</el-tab-pane>

View File

@ -157,19 +157,14 @@
<el-tag
effect="dark"
:color="submissionStatus.color"
@click.native="
handleRoute('/submission-detail/' + submissionId)
"
@click.native="reSubmit(submissionId)"
>
<i
class="el-icon-refresh"
@click="reSubmit(submissionId)"
></i>
<i class="el-icon-refresh"></i>
{{ submissionStatus.text }}
</el-tag>
</template>
<template
v-if="
v-else-if="
!this.contestID ||
(this.contestID &&
ContestRealTimePermission &&
@ -348,7 +343,7 @@ export default {
language: 'C',
isRemote: false,
theme: 'material',
theme: 'solarized',
submissionId: '',
submitted: false,
submitPwdVisible: false,

View File

@ -13,7 +13,7 @@
<span class="title">{{ status.statusName }}</span>
</template>
<template slot>
<div v-if="isCE || isSE" class="content">
<div v-if="isCE || isSE || isSF" class="content">
<pre>{{ submission.errorMessage }}</pre>
</div>
<div v-else class="content">
@ -331,6 +331,9 @@ export default {
isSE() {
return this.submission.status === JUDGE_STATUS_RESERVE.se;
},
isSF() {
return this.submission.status === JUDGE_STATUS_RESERVE.sf;
},
isAdminRole() {
return this.$store.getters.isAdminRole;
},

View File

@ -341,6 +341,8 @@ export default {
//
this.needCheckSubmitIds[row.submitId] = row.index;
this.checkStatusNum = 0;
if (!this.autoCheckOpen) {
//
this.checkSubmissionsStatus();
@ -541,6 +543,7 @@ export default {
//
this.needCheckSubmitIds[row.submitId] = row.index;
this.checkStatusNum = 0;
if (!this.autoCheckOpen) {
//
this.checkSubmissionsStatus();