增加比赛打印功能和账号限制功能

This commit is contained in:
Himit_ZH 2021-09-21 18:06:44 +08:00
parent 971f3f8506
commit 1e4215459e
47 changed files with 1682 additions and 388 deletions

View File

@ -16,5 +16,5 @@ features:
details: 判题使用 cgroup 隔离用户程序,网站权限控制完善
- title: 多样化
details: 独有自身判题服务同时支持其它知名OJ题目的提交判题
footer: MIT Licensed | Copyright © 2021.08.08 @Author Himit_ZH QQ Group:598587305
footer: MIT Licensed | Copyright © 2021.09.21 @Author Himit_ZH QQ Group:598587305
---

View File

@ -324,5 +324,79 @@ hoj-frontend:
## 四、更新最新版本
> 2021.09.21之后部署hoj的请看下面操作
请在对应的docker-compose.yml当前文件夹下执行`docker-compose pull`拉取最新镜像,然后重新`docker-compose up -d`即可。
> 2021.09.21之前部署hoj的请看下面操作
###1、修改MySQL8.0默认的密码加密方式
1进行hoj-mysql容器
```shell
docker exec -it hoj-mysql bash
```
(2) 输入对应的mysql密码进入mysql数据库
注意:-p 后面跟着数据库密码例如hoj123456
```shell
mysql -uroot -p数据库密码
```
3成功进入后执行以下命令
```shell
mysql> use mysql;
mysql> grant all PRIVILEGES on *.* to root@'%' WITH GRANT OPTION;
mysql> ALTER user 'root'@'%' IDENTIFIED BY '数据库密码' PASSWORD EXPIRE NEVER;
mysql> ALTER user 'root'@'%' IDENTIFIED WITH mysql_native_password BY '数据库密码';
mysql> FLUSH PRIVILEGES;
```
4 两次exit 退出mysql和容器
### 2、 添加hoj-mysql-checker模块
1可以选择拉取仓库最新的docker-compose.yml文件跟部署操作一样或者访问
https://gitee.com/himitzh0730/hoj-deploy/blob/master/standAlone/docker-compose.yml
2或者编辑docker-compose.yml文件手动添加新模块
```yaml
hoj-mysql-checker:
image: registry.cn-shenzhen.aliyuncs.com/hcode/hoj_database_checker
container_name: hoj-mysql-checker
depends_on:
- hoj-mysql
links:
- hoj-mysql:mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-hoj123456}
networks:
hoj-network:
ipv4_address: 172.20.0.8
```
(3) 保存后重启容器即可
```shell
docker-compose down
docker-compose up -d
```

View File

@ -10,6 +10,7 @@ import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.entity.UserInfo;
@ -136,7 +137,8 @@ public class AdminUserController {
.setUuid(uuid)
.setUsername(user.get(0))
.setPassword(SecureUtil.md5(user.get(1)))
.setEmail(user.get(2)));
.setEmail(StringUtils.isEmpty(user.get(2)) ? null : user.get(2))
.setRealname(user.get(3)));
userRoleList.add(new UserRole()
.setRoleId(1002L)
.setUid(uuid));

View File

@ -91,6 +91,9 @@ public class FileController {
@Autowired
private TagServiceImpl tagService;
@Autowired
private ContestPrintServiceImpl contestPrintService;
@RequestMapping("/generate-user-excel")
@RequiresAuthentication
@ -140,7 +143,7 @@ public class FileController {
//将文件保存指定目录
image.transferTo(FileUtil.file(Constants.File.USER_AVATAR_FOLDER.getPath() + File.separator + filename));
} catch (Exception e) {
log.error("头像文件上传异常-------------->{}", e.getMessage());
log.error("头像文件上传异常-------------->", e);
return CommonResult.errorResponse("服务器异常:头像上传失败!", CommonResult.STATUS_ERROR);
}
@ -189,7 +192,6 @@ public class FileController {
}
@RequestMapping(value = "/upload-carouse-img", method = RequestMethod.POST)
@RequiresAuthentication
@ResponseBody
@ -440,7 +442,7 @@ public class FileController {
.orderByAsc("time");
List<ContestRecord> contestRecordList = contestRecordService.list(wrapper);
Assert.notEmpty(contestRecordList, "比赛暂无排行榜记录!");
List<ACMContestRankVo> acmContestRankVoList = contestRecordService.calcACMRank(isOpenSealRank,contest,contestRecordList);
List<ACMContestRankVo> acmContestRankVoList = contestRecordService.calcACMRank(isOpenSealRank, contest, contestRecordList);
EasyExcel.write(response.getOutputStream())
.head(fileService.getContestRankExcelHead(contestProblemDisplayIDList, true))
.sheet("rank")
@ -575,7 +577,7 @@ public class FileController {
// 刷新缓存
bouts.flush();
} catch (IOException e) {
log.error("下载比赛AC提交代码的压缩文件异常------------>{}", e.getMessage());
log.error("下载比赛AC提交代码的压缩文件异常------------>", e);
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
@ -681,7 +683,7 @@ public class FileController {
//将文件保存指定目录
image.transferTo(FileUtil.file(Constants.File.MARKDOWN_FILE_FOLDER.getPath() + File.separator + filename));
} catch (Exception e) {
log.error("图片文件上传异常-------------->{}", e.getMessage());
log.error("图片文件上传异常-------------->", e);
return CommonResult.errorResponse("服务器异常:图片文件上传失败!", CommonResult.STATUS_ERROR);
}
@ -721,7 +723,7 @@ public class FileController {
//将文件保存指定目录
file.transferTo(FileUtil.file(Constants.File.MARKDOWN_FILE_FOLDER.getPath() + File.separator + filename));
} catch (Exception e) {
log.error("文件上传异常-------------->{}", e.getMessage());
log.error("文件上传异常-------------->", e);
return CommonResult.errorResponse("服务器异常:文件上传失败!", CommonResult.STATUS_ERROR);
}
@ -1242,7 +1244,7 @@ public class FileController {
try {
threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
log.error("线程池异常--------------->{}", e.getMessage());
log.error("线程池异常--------------->", e);
}
String fileName = "problem_export_" + System.currentTimeMillis() + ".zip";
@ -1266,7 +1268,7 @@ public class FileController {
}
bouts.flush();
} catch (IOException e) {
log.error("导出题目数据的压缩文件异常------------>{}", e.getMessage());
log.error("导出题目数据的压缩文件异常------------>", e);
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
@ -1297,5 +1299,63 @@ public class FileController {
}
}
@GetMapping("/download-contest-print-text")
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
public void downloadContestPrintText(@RequestParam("id") Long id,
HttpServletResponse response) {
ContestPrint contestPrint = contestPrintService.getById(id);
String filename = contestPrint.getUsername() + "_Contest_Print.txt";
String filePath = Constants.File.CONTEST_TEXT_PRINT_FOLDER.getPath() + File.separator +filename;
if (!FileUtil.exist(filePath)) {
FileWriter fileWriter = new FileWriter(filePath);
fileWriter.write(contestPrint.getContent());
}
FileReader zipFileReader = new FileReader(filePath);
BufferedInputStream bins = new BufferedInputStream(zipFileReader.getInputStream());//放到缓冲流里面
OutputStream outs = null;//获取文件输出IO流
BufferedOutputStream bouts = null;
try {
outs = response.getOutputStream();
bouts = new BufferedOutputStream(outs);
response.setContentType("application/x-download");
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
int bytesRead = 0;
byte[] buffer = new byte[1024 * 10];
//开始向网络传输文件流
while ((bytesRead = bins.read(buffer, 0, 1024 * 10)) != -1) {
bouts.write(buffer, 0, bytesRead);
}
// 刷新缓存
bouts.flush();
} catch (IOException e) {
log.error("下载比赛打印文本文件异常------------>", e);
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, Object> map = new HashMap<>();
map.put("status", CommonResult.STATUS_ERROR);
map.put("msg", "下载文件失败,请重新尝试!");
map.put("data", null);
try {
response.getWriter().println(JSONUtil.toJsonStr(map));
} catch (IOException ioException) {
ioException.printStackTrace();
}
} finally {
try {
bins.close();
if (outs != null) {
outs.close();
}
if (bouts != null) {
bouts.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

View File

@ -576,8 +576,8 @@ public class AccountController {
String realname = (String) params.get("realname");
String nickname = (String) params.get("nickname");
if (!StringUtils.isEmpty(realname) && realname.length() > 10) {
return CommonResult.errorResponse("真实姓名不能超过10位");
if (!StringUtils.isEmpty(realname) && realname.length() > 50) {
return CommonResult.errorResponse("真实姓名不能超过50位");
}
if (!StringUtils.isEmpty(nickname) && nickname.length() > 20) {
return CommonResult.errorResponse("昵称不能超过20位");

View File

@ -0,0 +1,187 @@
package top.hcode.hoj.controller.oj;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.dto.CheckACDto;
import top.hcode.hoj.pojo.entity.Contest;
import top.hcode.hoj.pojo.entity.ContestPrint;
import top.hcode.hoj.pojo.entity.ContestRecord;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.ContestRecordService;
import top.hcode.hoj.service.impl.ContestPrintServiceImpl;
import top.hcode.hoj.service.impl.ContestServiceImpl;
import top.hcode.hoj.utils.Constants;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @Author: Himit_ZH
* @Date: 2021/9/20 13:15
* @Description: 处理比赛管理模块的相关数据请求
*/
@RestController
@RequestMapping("/api")
public class ContestAdminController {
@Autowired
private ContestServiceImpl contestService;
@Autowired
private ContestRecordService contestRecordService;
@Autowired
private ContestPrintServiceImpl contestPrintService;
/**
* @MethodName getContestACInfo
* @Params * @param null
* @Description 获取各个用户的ac情况仅限于比赛管理者可查看
* @Return
* @Since 2021/1/17
*/
@GetMapping("/get-contest-ac-info")
@RequiresAuthentication
public CommonResult getContestACInfo(@RequestParam("cid") Long cid,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
@RequestParam(value = "limit", required = false) Integer limit,
HttpServletRequest request) {
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 获取本场比赛的状态
Contest contest = contestService.getById(cid);
// 超级管理员或者该比赛的创建者则为比赛管理者
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
if (!isRoot && !contest.getUid().equals(userRolesVo.getUid())) {
return CommonResult.errorResponse("对不起,你无权查看!", CommonResult.STATUS_FORBIDDEN);
}
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 30;
// 获取当前比赛的状态为ac未被校验的排在签名
IPage<ContestRecord> contestRecords = contestRecordService.getACInfo(currentPage,
limit, Constants.Contest.RECORD_AC.getCode(), cid, contest.getUid());
return CommonResult.successResponse(contestRecords, "查询成功");
}
/**
* @MethodName checkContestACInfo
* @Params * @param null
* @Description 比赛管理员确定该次提交的ac情况
* @Return
* @Since 2021/1/17
*/
@PutMapping("/check-contest-ac-info")
@RequiresAuthentication
public CommonResult checkContestACInfo(@RequestBody CheckACDto checkACDto,
HttpServletRequest request) {
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 获取本场比赛的状态
Contest contest = contestService.getById(checkACDto.getCid());
// 超级管理员或者该比赛的创建者则为比赛管理者
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
if (!isRoot && !contest.getUid().equals(userRolesVo.getUid())) {
return CommonResult.errorResponse("对不起,你无权操作!", CommonResult.STATUS_FORBIDDEN);
}
boolean result = contestRecordService.updateById(
new ContestRecord().setChecked(checkACDto.getChecked()).setId(checkACDto.getId()));
if (result) {
return CommonResult.successResponse(null, "修改校验确定成功!");
} else {
return CommonResult.errorResponse("修改校验确定失败!");
}
}
@GetMapping("/get-contest-print")
@RequiresAuthentication
public CommonResult getContestPrint(@RequestParam("cid") Long cid,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
@RequestParam(value = "limit", required = false) Integer limit,
HttpServletRequest request) {
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 获取本场比赛的状态
Contest contest = contestService.getById(cid);
// 超级管理员或者该比赛的创建者则为比赛管理者
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
if (!isRoot && !contest.getUid().equals(userRolesVo.getUid())) {
return CommonResult.errorResponse("对不起,你无权查看!", CommonResult.STATUS_FORBIDDEN);
}
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 30;
// 获取当前比赛的未被确定的排在签名
IPage<ContestPrint> contestPrintIPage = new Page<>(currentPage,limit);
QueryWrapper<ContestPrint> contestPrintQueryWrapper = new QueryWrapper<>();
contestPrintQueryWrapper.select("id","cid","username","realname","status","gmt_create")
.eq("cid", cid)
.orderByAsc("status")
.orderByDesc("gmt_create");
IPage<ContestPrint> contestPrintList = contestPrintService.page(contestPrintIPage, contestPrintQueryWrapper);
return CommonResult.successResponse(contestPrintList, "查询成功");
}
/**
* @param id
* @param cid
* @param request
* @MethodName checkContestStatus
* @Description 更新该打印为确定状态
* @Return
* @Since 2021/9/20
*/
@PutMapping("/check-contest-print-status")
@RequiresAuthentication
public CommonResult checkContestStatus(@RequestParam("id") Long id,
@RequestParam("cid") Long cid,
HttpServletRequest request) {
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 获取本场比赛的状态
Contest contest = contestService.getById(cid);
// 超级管理员或者该比赛的创建者则为比赛管理者
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
if (!isRoot && !contest.getUid().equals(userRolesVo.getUid())) {
return CommonResult.errorResponse("对不起,你无权操作!", CommonResult.STATUS_FORBIDDEN);
}
boolean result = contestPrintService.updateById(new ContestPrint().setId(id).setStatus(1));
if (result) {
return CommonResult.successResponse(null, "确定成功!");
} else {
return CommonResult.errorResponse("确定失败!");
}
}
}

View File

@ -1,6 +1,7 @@
package top.hcode.hoj.controller.oj;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ReUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -8,9 +9,11 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.dto.CheckACDto;
import top.hcode.hoj.pojo.dto.ContestPrintDto;
import top.hcode.hoj.pojo.dto.UserReadContestAnnouncementDto;
import top.hcode.hoj.pojo.entity.*;
import top.hcode.hoj.pojo.vo.*;
@ -72,6 +75,9 @@ public class ContestController {
@Autowired
private CodeTemplateServiceImpl codeTemplateService;
@Autowired
private ContestPrintServiceImpl contestPrintService;
/**
* @MethodName getContestList
* @Params * @param null
@ -152,6 +158,19 @@ public class ContestController {
return CommonResult.errorResponse("比赛密码错误!");
}
/**
*
* 需要校验当前比赛是否开启账号规则限制如果有需要对当前用户的用户名进行验证
*
*/
if (contest.getOpenAccountLimit()
&&!contestService.checkAccountRule(contest.getAccountLimitRule(),userRolesVo.getUsername())){
return CommonResult.errorResponse("对不起!本次比赛只允许特定账号规则的用户参赛!",CommonResult.STATUS_ACCESS_DENIED);
}
QueryWrapper<ContestRegister> wrapper = new QueryWrapper<ContestRegister>().eq("cid", Long.valueOf(cidStr))
.eq("uid", userRolesVo.getUid());
if (contestRegisterService.getOne(wrapper) != null) {
@ -415,7 +434,7 @@ public class ContestController {
searchStatus, searchUsername, uid, beforeContestSubmit, rule, contest.getStartTime(), sealRankTime, userRolesVo.getUid());
if (commonJudgeList.getTotal() == 0) { // 未查询到一条数据
return CommonResult.successResponse(null, "暂无数据");
return CommonResult.successResponse(commonJudgeList, "暂无数据");
} else {
// 比赛还是进行阶段同时不是超级管理员与比赛管理员需要将除自己之外的提交的时间空间长度隐藏
@ -578,75 +597,47 @@ public class ContestController {
}
/**
* @MethodName getContestACInfo
* @Params * @param null
* @Description 获取各个用户的ac情况仅限于比赛管理者可查看
* @param contestPrintDto
* @param request
* @MethodName submitPrintText
* @Description 提交比赛文本打印内容
* @Return
* @Since 2021/1/17
* @Since 2021/9/20
*/
@GetMapping("/get-contest-ac-info")
@PostMapping("/submit-print-text")
@RequiresAuthentication
public CommonResult getContestACInfo(@RequestParam("cid") Long cid,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
@RequestParam(value = "limit", required = false) Integer limit,
HttpServletRequest request) {
public CommonResult submitPrintText(@RequestBody ContestPrintDto contestPrintDto,
HttpServletRequest request) {
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 获取本场比赛的状态
Contest contest = contestService.getById(cid);
Contest contest = contestService.getById(contestPrintDto.getCid());
// 超级管理员或者该比赛的创建者则为比赛管理者
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
if (!isRoot && !contest.getUid().equals(userRolesVo.getUid())) {
return CommonResult.errorResponse("对不起,你无权查看!", CommonResult.STATUS_FORBIDDEN);
/**
* 需要对该比赛做判断是否处于开始或结束状态才可以提交打印内容同时若是私有赛需要判断是否已注册比赛管理员包括超级管理员可以直接获取
*/
CommonResult commonResult = contestService.checkContestAuth(contest, userRolesVo, isRoot);
if (commonResult != null) {
return commonResult;
}
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 30;
// 获取当前比赛的状态为ac未被校验的排在签名
IPage<ContestRecord> contestRecords = contestRecordService.getACInfo(currentPage,
limit, Constants.Contest.RECORD_AC.getCode(), cid, contest.getUid());
return CommonResult.successResponse(contestRecords, "查询成功");
}
/**
* @MethodName checkContestACInfo
* @Params * @param null
* @Description 比赛管理员确定该次提交的ac情况
* @Return
* @Since 2021/1/17
*/
@PutMapping("/check-contest-ac-info")
@RequiresAuthentication
public CommonResult checkContestACInfo(@RequestBody CheckACDto checkACDto,
HttpServletRequest request) {
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 获取本场比赛的状态
Contest contest = contestService.getById(checkACDto.getCid());
// 超级管理员或者该比赛的创建者则为比赛管理者
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
if (!isRoot && !contest.getUid().equals(userRolesVo.getUid())) {
return CommonResult.errorResponse("对不起,你无权操作!", CommonResult.STATUS_FORBIDDEN);
}
boolean result = contestRecordService.updateById(
new ContestRecord().setChecked(checkACDto.getChecked()).setId(checkACDto.getId()));
boolean result = contestPrintService.saveOrUpdate(new ContestPrint().setCid(contestPrintDto.getCid())
.setContent(contestPrintDto.getContent())
.setUsername(userRolesVo.getUsername())
.setRealname(userRolesVo.getRealname()));
if (result) {
return CommonResult.successResponse(null, "修改校验确定成功");
return CommonResult.successResponse(null, "提交成功,请等待工作人员打印!");
} else {
return CommonResult.errorResponse("修改校验确定失败!");
return CommonResult.errorResponse("提交失败!");
}
}
}

View File

@ -53,9 +53,10 @@ public class DiscussionController {
@RequestParam(value = "currentPage", required = false, defaultValue = "1") Integer currentPage,
@RequestParam(value = "cid", required = false) Integer categoryId,
@RequestParam(value = "pid", required = false) String pid,
@RequestParam(value = "onlyMine", required = false,defaultValue = "false") Boolean onlyMine,
@RequestParam(value = "onlyMine", required = false, defaultValue = "false") Boolean onlyMine,
@RequestParam(value = "keyword", required = false) String keyword,
HttpServletRequest request){
@RequestParam(value = "admin", defaultValue = "false") Boolean admin,
HttpServletRequest request) {
QueryWrapper<Discussion> discussionQueryWrapper = new QueryWrapper<>();
@ -79,15 +80,18 @@ public class DiscussionController {
discussionQueryWrapper.eq("pid", pid);
}
boolean isAdmin = SecurityUtils.getSubject().hasRole("root")
|| SecurityUtils.getSubject().hasRole("problem_admin")
|| SecurityUtils.getSubject().hasRole("admin");
discussionQueryWrapper
.eq("status", 0)
.eq(!(admin && isAdmin), "status", 0)
.orderByDesc("top_priority")
.orderByDesc("gmt_modified")
.orderByDesc("like_num")
.orderByDesc("view_num");
if (onlyMine){
// 获取当前登录的用户
if (onlyMine) {
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
discussionQueryWrapper.eq("uid", userRolesVo.getUid());
@ -122,7 +126,7 @@ public class DiscussionController {
return CommonResult.errorResponse("对不起,该讨论不存在!", CommonResult.STATUS_NOT_FOUND);
}
if (discussion.getStatus() == 2) {
if (discussion.getStatus() == 1) {
return CommonResult.errorResponse("对不起,该讨论已被封禁!", CommonResult.STATUS_FORBIDDEN);
}
@ -167,11 +171,11 @@ public class DiscussionController {
@PutMapping("/discussion")
@RequiresPermissions("discussion_edit")
@RequiresAuthentication
public CommonResult updateDiscussion(@RequestBody Discussion discussion) {
public CommonResult updateDiscussion(@RequestBody Discussion discussion) {
boolean isOk = discussionService.updateById(discussion);
if (isOk){
if (isOk) {
return CommonResult.successResponse(null, "修改成功");
}else{
} else {
return CommonResult.errorResponse("修改失败");
}
}
@ -188,7 +192,7 @@ public class DiscussionController {
// 如果不是是管理员,则需要附加当前用户的uid条件
if (!SecurityUtils.getSubject().hasRole("root")
&& !SecurityUtils.getSubject().hasRole("admin")
&&!SecurityUtils.getSubject().hasRole("problem_admin")) {
&& !SecurityUtils.getSubject().hasRole("problem_admin")) {
discussionUpdateWrapper.eq("uid", userRolesVo.getUid());
}
boolean isOk = discussionService.remove(discussionUpdateWrapper);
@ -252,14 +256,14 @@ public class DiscussionController {
/**
* @MethodName addDiscussionReport
* @Params * @param uid content reporter
* @Params * @param uid content reporter
* @Description 添加讨论举报
* @Return
* @Since 2021/5/11
*/
@PostMapping("/discussion-report")
@RequiresAuthentication
public CommonResult addDiscussionReport(@RequestBody DiscussionReport discussionReport){
public CommonResult addDiscussionReport(@RequestBody DiscussionReport discussionReport) {
boolean isOk = discussionReportService.saveOrUpdate(discussionReport);
if (isOk) {
return CommonResult.successResponse(null, "举报成功");

View File

@ -465,7 +465,7 @@ public class JudgeController {
if (commonJudgeList.getTotal() == 0) { // 未查询到一条数据
return CommonResult.successResponse(null, "暂无数据");
return CommonResult.successResponse(commonJudgeList, "暂无数据");
} else {
return CommonResult.successResponse(commonJudgeList, "获取成功");
}

View File

@ -0,0 +1,16 @@
package top.hcode.hoj.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import top.hcode.hoj.pojo.entity.ContestPrint;
/**
* @Author: Himit_ZH
* @Date: 2021/9/19 21:04
* @Description:
*/
@Mapper
@Repository
public interface ContestPrintMapper extends BaseMapper<ContestPrint> {
}

View File

@ -19,7 +19,7 @@
order by c.status ASC, c.start_time DESC
</select>
<select id="getContestInfoById" resultType="top.hcode.hoj.pojo.vo.ContestVo" useCache="true">
select c.id,c.author,c.title,c.type,c.status,c.description,c.seal_rank,c.seal_rank_time,c.source,c.auth,c.start_time,c.end_time,c.duration
select c.id,c.author,c.open_print,c.title,c.type,c.status,c.description,c.seal_rank,c.seal_rank_time,c.source,c.auth,c.start_time,c.end_time,c.duration
from contest c where c.id = #{cid} and c.visible=true
</select>
<select id="getWithinNext14DaysContests" resultType="top.hcode.hoj.pojo.vo.ContestVo">

View File

@ -0,0 +1,20 @@
package top.hcode.hoj.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @Author: Himit_ZH
* @Date: 2021/9/20 13:00
* @Description:
*/
@Data
public class ContestPrintDto {
@NotBlank(message = "比赛id不能为空")
private Long cid;
@NotBlank(message = "打印内容不能为空")
private String content;
}

View File

@ -57,6 +57,9 @@ public class ContestVo implements Serializable {
@ApiModelProperty(value = "是否开启封榜")
private Boolean sealRank;
@ApiModelProperty(value = "是否打开打印功能")
private Boolean openPrint;
@ApiModelProperty(value = "封榜起始时间,一直到比赛结束,不刷新榜单")
private Date sealRankTime;
}

View File

@ -0,0 +1,12 @@
package top.hcode.hoj.service;
import com.baomidou.mybatisplus.extension.service.IService;
import top.hcode.hoj.pojo.entity.ContestPrint;
/**
* @Author: Himit_ZH
* @Date: 2021/9/19 21:05
* @Description:
*/
public interface ContestPrintService extends IService<ContestPrint> {
}

View File

@ -26,4 +26,6 @@ public interface ContestService extends IService<Contest> {
Boolean isSealRank(String uid, Contest contest, Boolean forceRefresh, Boolean isRoot);
CommonResult checkJudgeAuth(Contest contest, String uid);
boolean checkAccountRule(String accountRule, String username);
}

View File

@ -5,6 +5,8 @@ public interface ScheduleService {
void deleteTestCase();
void deleteContestPrintText();
void getOjContestsList();
void getCodeforcesRating();

View File

@ -0,0 +1,17 @@
package top.hcode.hoj.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import top.hcode.hoj.dao.ContestPrintMapper;
import top.hcode.hoj.pojo.entity.ContestPrint;
import top.hcode.hoj.service.ContestPrintService;
/**
* @Author: Himit_ZH
* @Date: 2021/9/19 21:05
* @Description:
*/
@Service
public class ContestPrintServiceImpl extends ServiceImpl<ContestPrintMapper, ContestPrint> implements ContestPrintService {
}

View File

@ -1,8 +1,10 @@
package top.hcode.hoj.service.impl;
import cn.hutool.core.util.ReUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.entity.ContestRegister;
import top.hcode.hoj.pojo.vo.ContestVo;
@ -14,7 +16,9 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import top.hcode.hoj.utils.Constants;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* <p>
@ -65,6 +69,11 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, Contest> impl
if (!isRoot && !contest.getUid().equals(userRolesVo.getUid())) { // 若不是比赛管理者
if (contest.getOpenAccountLimit()
&&!checkAccountRule(contest.getAccountLimitRule(),userRolesVo.getUsername())){
return CommonResult.errorResponse("对不起!本次比赛只允许特定账号规则的用户参赛!",CommonResult.STATUS_ACCESS_DENIED);
}
// 判断一下比赛的状态还未开始不能查看题目
if (contest.getStatus().intValue() != Constants.Contest.STATUS_RUNNING.getCode() &&
contest.getStatus().intValue() != Constants.Contest.STATUS_ENDED.getCode()) {
@ -113,4 +122,28 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, Contest> impl
}
return null;
}
@Override
public boolean checkAccountRule(String accountRule, String username) {
String prefix = ReUtil.get("<prefix>([\\s\\S]*?)</prefix>",
accountRule, 1);
String suffix = ReUtil.get("<suffix>([\\s\\S]*?)</suffix>",
accountRule, 1);
String start = ReUtil.get("<start>([\\s\\S]*?)</start>",
accountRule, 1);
String end = ReUtil.get("<end>([\\s\\S]*?)</end>",
accountRule, 1);
int startNum = Integer.parseInt(start);
int endNum = Integer.parseInt(end);
for (int i = startNum; i <= endNum; i++) {
if (username.equals(prefix + i + suffix)) {
return true;
}
}
return false;
}
}

View File

@ -436,8 +436,8 @@ public class ProblemServiceImpl extends ServiceImpl<ProblemMapper, Problem> impl
result.set("isSpj", isSpj);
result.set("version", version);
result.set("testCasesSize", problemCaseList.size());
result.set("testCases", new JSONArray());
JSONArray testCaseList = new JSONArray(problemCaseList.size());
for (ProblemCase problemCase : problemCaseList) {
JSONObject jsonObject = new JSONObject();
@ -469,9 +469,11 @@ public class ProblemServiceImpl extends ServiceImpl<ProblemMapper, Problem> impl
jsonObject.set("EOFStrippedOutputMd5", DigestUtils.md5DigestAsHex(rtrim(output).getBytes()));
}
((JSONArray) result.get("testCases")).put(jsonObject);
testCaseList.add(jsonObject);
}
result.set("testCases", testCaseList);
FileWriter infoFile = new FileWriter(testCasesDir + "/info", CharsetUtil.UTF_8);
// 写入记录文件
infoFile.write(JSONUtil.toJsonStr(result));
@ -491,7 +493,8 @@ public class ProblemServiceImpl extends ServiceImpl<ProblemMapper, Problem> impl
result.set("isSpj", isSpj);
result.set("version", version);
result.set("testCasesSize", problemCaseList.size());
result.set("testCases", new JSONArray());
JSONArray testCaseList = new JSONArray(problemCaseList.size());
String testCasesDir = Constants.File.TESTCASE_BASE_FOLDER.getPath() + File.separator + "problem_" + problemId;
FileUtil.del(testCasesDir);
@ -530,9 +533,11 @@ public class ProblemServiceImpl extends ServiceImpl<ProblemMapper, Problem> impl
jsonObject.set("EOFStrippedOutputMd5", DigestUtils.md5DigestAsHex(rtrim(outputData).getBytes()));
}
((JSONArray) result.get("testCases")).put(index, jsonObject);
testCaseList.add(jsonObject);
}
result.set("testCases", testCaseList);
FileWriter infoFile = new FileWriter(testCasesDir + "/info", CharsetUtil.UTF_8);
// 写入记录文件
infoFile.write(JSONUtil.toJsonStr(result));

View File

@ -125,6 +125,21 @@ public class ScheduleServiceImpl implements ScheduleService {
}
}
/**
* @MethodName deleteContestPrintText
* @Params * @param null
* @Description 每天4点定时删除本地的比赛打印数据
* @Return
* @Since 2021/9/19
*/
@Scheduled(cron = "0 0 4 * * *")
@Override
public void deleteContestPrintText() {
boolean result = FileUtil.del(Constants.File.CONTEST_TEXT_PRINT_FOLDER.getPath());
if (!result) {
log.error("每日定时任务异常------------------------>{}", "清除本地的比赛打印数据失败!");
}
}
/**
* 每两小时获取其他OJ的比赛列表并保存在redis里

View File

@ -162,6 +162,8 @@ public class Constants {
MARKDOWN_FILE_FOLDER("/hoj/file/md"),
CONTEST_TEXT_PRINT_FOLDER("/hoj/file/contest_print"),
IMG_API("/api/public/img/"),
FILE_API("/api/public/file/"),

View File

@ -31,8 +31,6 @@
<fileNamePattern>${logging.path}/hoj.info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--保存时长-->
<MaxHistory>15</MaxHistory>
<!--单个文件最大-->
<maxFileSize>200MB</maxFileSize>
<!--总大小-->
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
@ -53,8 +51,6 @@
<fileNamePattern>${logging.path}/hoj.error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--保存时长-->
<MaxHistory>15</MaxHistory>
<!--单个文件最大-->
<maxFileSize>200MB</maxFileSize>
<!--总大小-->
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>

View File

@ -47,7 +47,8 @@ public class ProblemTestCaseUtils {
result.set("isSpj", isSpj);
result.set("version", version);
result.set("testCasesSize", testCases.size());
result.set("testCases", new JSONArray());
JSONArray testCaseList = new JSONArray(testCases.size());
String testCasesDir = Constants.JudgeDir.TEST_CASE_DIR.getContent() + "/problem_" + problemId;
@ -84,9 +85,11 @@ public class ProblemTestCaseUtils {
jsonObject.set("EOFStrippedOutputMd5", DigestUtils.md5DigestAsHex(rtrim(outputData).getBytes()));
}
((JSONArray) result.get("testCases")).put(index, jsonObject);
testCaseList.add(jsonObject);
}
result.set("testCases",testCaseList);
FileWriter infoFile = new FileWriter(testCasesDir + File.separator + "info", CharsetUtil.UTF_8);
// 写入记录文件
infoFile.write(JSONUtil.toJsonStr(result));

View File

@ -31,8 +31,6 @@
<fileNamePattern>${logging.path}/hoj.info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--保存时长-->
<MaxHistory>15</MaxHistory>
<!--单个文件最大-->
<maxFileSize>200MB</maxFileSize>
<!--总大小-->
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
@ -53,8 +51,6 @@
<fileNamePattern>${logging.path}/hoj.error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--保存时长-->
<MaxHistory>15</MaxHistory>
<!--单个文件最大-->
<maxFileSize>200MB</maxFileSize>
<!--总大小-->
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>

View File

@ -78,6 +78,15 @@ public class Contest implements Serializable {
@ApiModelProperty(value = "是否可见")
private Boolean visible;
@ApiModelProperty(value = "是否打开打印功能")
private Boolean openPrint;
@ApiModelProperty(value = "是否打开账号限制")
private Boolean openAccountLimit;
@ApiModelProperty(value = "账号限制规则 <prefix>**</prefix><suffix>**</suffix><start>**</start><end>**</end>")
private String accountLimitRule;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;

View File

@ -0,0 +1,50 @@
package top.hcode.hoj.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* @Author: Himit_ZH
* @Date: 2021/9/19 21:00
* @Description:
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="ContestPrint", description="")
public class ContestPrint {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long cid;
@ApiModelProperty(value = "提交打印文本的用户")
private String username;
@ApiModelProperty(value = "真实姓名")
private String realname;
@ApiModelProperty(value = "内容")
private String content;
@ApiModelProperty(value = "状态")
private Integer status;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
}

View File

@ -169,9 +169,8 @@ export default {
}
body {
background-color: #eff3f5 !important;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Arial, sans-serif !important;
-webkit-font-smoothing: antialiased !important;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', '微软雅黑', Arial, sans-serif !important;
color: #495060 !important;
font-size: 12px !important;
}
@ -181,6 +180,28 @@ pre,
samp {
font-family: Consolas, Menlo, Courier, monospace;
}
::-webkit-scrollbar {
width: 10px;
height: 12px;
background-color: #fff;
}
::-webkit-scrollbar-thumb {
display: block;
min-height: 12px;
min-width: 10px;
border-radius: 6px;
background-color: rgb(217, 217, 217);
}
::-webkit-scrollbar-thumb:hover {
display: block;
min-height: 12px;
min-width: 10px;
border-radius: 6px;
background-color: rgb(159, 159, 159);
}
#admin-content {
background-color: #1e9fff;
position: absolute;

View File

@ -405,6 +405,29 @@ const ojApi = {
data
})
},
// 提交打印文本
submitPrintText(data){
return ajax('/api/submit-print-text', 'post', {
data
})
},
// 获取比赛打印文本列表
getContestPrintList(params){
return ajax('/api/get-contest-print', 'get', {
params
})
},
// 更新比赛打印的状态
updateContestPrintStatus(params){
return ajax('/api/check-contest-print-status', 'put', {
params
})
},
// 比赛题目对应的提交重判
ContestRejudgeProblem(params){
return ajax('/api/admin/judge/rejudge-contest-problem', 'get', {

View File

@ -110,7 +110,7 @@ function getLanguages (all=true) {
}
function stringToExamples(value){
let reg = "<input>([\\s\\S]+?)</input><output>([\\s\\S]+?)</output>";
let reg = "<input>([\\s\\S]*?)</input><output>([\\s\\S]*?)</output>";
let re = RegExp(reg,"g");
let objList = []
let tmp;

View File

@ -290,7 +290,7 @@ export default {
<style>
.CodeMirror {
height: 545px !important;
height: 600px !important;
}
.CodeMirror-scroll {
min-height: 549px;

View File

@ -131,7 +131,7 @@
<el-row :gutter="30" justify="space-around">
<el-col :md="10" :xs="24">
<el-form-item :label="$t('m.RealName')">
<el-input v-model="formProfile.realname" :maxlength="10" />
<el-input v-model="formProfile.realname" :maxlength="50" />
</el-form-item>
<el-form-item :label="$t('m.Nickname')">
<el-input v-model="formProfile.nickname" :maxlength="20" />
@ -140,7 +140,7 @@
<el-input v-model="formProfile.school" :maxlength="50" />
</el-form-item>
<el-form-item :label="$t('m.Student_Number')">
<el-input v-model="formProfile.number" :maxlength="25" />
<el-input v-model="formProfile.number" :maxlength="20" />
</el-form-item>
</el-col>
<el-col :md="4" :lg="4">

View File

@ -62,8 +62,8 @@ export const m = {
Delete_User:'Delete User',
Import_User: 'Import User',
Import_User_Tips1:'The imported user data only supports user data in CSV format.',
Import_User_Tips2:'There are three columns of data: user name, password, and mailbox. Any column cannot be empty, otherwise the data in this row may fail to be imported.',
Import_User_Tips3:'The first line does not need to write the three column names ("username", "password", "email").',
Import_User_Tips2:'There are three columns of data: username, password, email, and realname. The username and password cannot be empty, email and realname can be enmpty, otherwise the data in this row may fail to be imported.',
Import_User_Tips3:'The first line does not need to write the three column names ("username", "password", "email","realname").',
Import_User_Tips4:'Please import the file saved as UTF-8 code, otherwise Chinese may be garbled.',
Choose_File:'Choose File',
Password: 'Password',
@ -88,7 +88,7 @@ export const m = {
The_number_of_users_selected_cannot_be_empty:'The number of users selected cannot be empty',
Error_Please_check_your_choice:'Wrong, please check your choice.',
Generate_User_Success:'All users in the specified format have been created successfully, and the user table has been downloaded to your computer successfully!',
Generate_Skipped_Reason:'rows user data are filtered because it may be an empty row or a column value is empty.',
Generate_Skipped_Reason:'rows user data are filtered because it may be an empty row or a column(username or password) value is empty.',
Upload_Users_Successfully:'Upload Users Successfully',
// /views/admin/general/Announcement.vue
@ -222,7 +222,6 @@ export const m = {
View_Contest_Announcement_List:'View Contest Announcement List',
Download_Contest_AC_Submission:'Download Contest AC Submissions',
Exclude_admin_submissions:'Exclude admin submissions',
Delete_Contest_Tips:'This operation will delete the contest and its submission, discussion, announcement, record and other data. Do you want to continue?',
// /views/admin/contest/Contest.vue
@ -244,6 +243,11 @@ export const m = {
Create_Contest:'Create Contest',
Contest_Duration_Check:'The duration of the contest cannot be less than or equal to zero!',
Contets_Time_Check:'The start time should be earlier than the end time!',
Print_Func:'Print Function',
Not_Support_Print:'Not Support Print',
Support_Offline_Print:'Support Offline Print',
Account_Limit:'Account Limit',
The_allowed_account_will_be:'The allowed username will be ',
// /views/admin/discussion/Discussion.vue
Discussion_ID:'Discussion ID',

View File

@ -62,8 +62,8 @@ export const m = {
Delete_User:'删除用户',
Import_User: '导入用户',
Import_User_Tips1:'用户数据导入仅支持csv格式的用户数据。',
Import_User_Tips2:'共三列数据:用户名,密码,邮箱,任一列不能为空,否则该行数据可能导入失败。',
Import_User_Tips3:'第一行不必写(“用户名”,“密码”,“邮箱”)这三个列名',
Import_User_Tips2:'共三列数据:用户名和密码不能为空,邮箱和真实姓名可选填,否则该行数据可能导入失败。',
Import_User_Tips3:'第一行不必写(“用户名”,“密码”,“邮箱”"真实姓名")这三个列名',
Import_User_Tips4:'请导入保存为UTF-8编码的文件否则中文可能会乱码。',
Choose_File:'选择文件',
Password: '密码',
@ -88,7 +88,7 @@ export const m = {
The_number_of_users_selected_cannot_be_empty:'选择的用户不能为空',
Error_Please_check_your_choice:'错误,请检查你的输入或选择是否准确',
Generate_User_Success:'所有用户已经被成功创建, 用户的列表数据文件将下载到你的电脑里',
Generate_Skipped_Reason:'行用户数据被过滤,原因是可能为空行或某个列值为空',
Generate_Skipped_Reason:'行用户数据被过滤,原因是可能为空行或某个列值(用户名或密码)为空',
Upload_Users_Successfully:'上传用户成功',
// /views/admin/general/Announcement.vue
@ -241,6 +241,11 @@ export const m = {
Create_Contest:'创建比赛',
Contest_Duration_Check:'比赛时长不能小于0',
Contets_Time_Check:'开始时间应该早于结束时间',
Print_Func:'打印功能',
Not_Support_Print:'不支持打印',
Support_Offline_Print:'支持线下打印',
Account_Limit:'账号限制',
The_allowed_account_will_be:'允许参加比赛的用户名是:',
// /views/admin/discussion/Discussion.vue
Discussion_ID:'讨论ID',

View File

@ -164,6 +164,9 @@ export const m = {
Good_luck_to_you:'Good luck to you!',
// /views/oj/problem/Problem.vue
Problem_Description:'Problem Description',
My_Submission:'My Submission',
Login_to_view_your_submission_history:'Login to view your submission history',
Shrink_Sidebar:'Shrink Sidebar',
View_Problem_Content:'View Problem Content',
Only_View_Problem:'Only View Problem',
@ -172,7 +175,7 @@ export const m = {
Show_Tags:'Show tags',
No_tag:'No tag',
Statistic: 'Statistic',
Solution:'Solution',
Solutions:'Solutions',
Problem_Discussion:'Discussion',
Description: 'Description',
Input: 'Input',
@ -311,6 +314,8 @@ export const m = {
Submissions: 'Submissions',
Rankings: 'Rankings',
Comment:'Comment',
Print:'Print',
Admin_Print:'Admin Print',
Admin_Helper: 'AC Info',
Register_contest_successfully:'Register contest successfully',
Please_check_the_contest_announcement_for_details:'Please check the contest announcement for details',
@ -336,6 +341,19 @@ export const m = {
Check_It: 'Check It',
Accepted:'Accepted',
// /views/oj/contest/children/ContestPrint.vue
Print_Title:'Contest Text Printing',
Print_tips:'Please put the text to be printed into the content box, and then submit. Note: please do not submit maliciously!',
Content:'Content',
Content_cannot_be_empty:'Tne content cannot be empty!',
The_number_of_content_cannot_be_less_than_50:'The number of words cannot be less than 50',
Success_submit_tips:'Submitted successfully! Please wait patiently for the staff to print!',
// /views/oj/contest/children/ContestAdminPrint.vue
Download:'Download',
Printed:'Printed',
Not_Printed:'Not Printed',
// /views/oj/contest/children/ContestRejudgeAdmin.vue
Contest_Rejudge:'Contest Rejudge',
ID: 'ID',

View File

@ -165,6 +165,9 @@ export const m = {
Good_luck_to_you:'祝你好运!',
// /views/oj/problem/Problem.vue
Problem_Description:'题目描述',
My_Submission:'我的提交',
Login_to_view_your_submission_history:'登录以查看您的提交记录',
Shrink_Sidebar:'收缩侧边栏',
View_Problem_Content:'查看题目内容',
Only_View_Problem:'只看题目内容',
@ -173,7 +176,7 @@ export const m = {
Show_Tags:'显示标签',
No_tag:'暂无标签',
Statistic: '题目统计',
Solution:'提交记录',
Solutions:'全部提交',
Problem_Discussion:'题目讨论',
Description: '题目描述',
Input: '输入描述',
@ -214,7 +217,7 @@ export const m = {
Mine:'我的',
ID: 'ID',
Time: '运行时间',
Memory: '内存',
Memory: '运行内存',
Length:'代码长度',
Language:'语言',
View_submission_details:'查看提交详情',
@ -314,6 +317,8 @@ export const m = {
Submissions: '提交记录',
Rankings: '排行榜',
Comment:'评论',
Print:'打印',
Admin_Print:'管理打印',
Admin_Helper: 'AC助手',
Register_contest_successfully:'比赛报名成功',
Please_check_the_contest_announcement_for_details:'具体内容请查看比赛公告',
@ -339,6 +344,19 @@ export const m = {
Check_It: '检查',
Accepted:'Accepted',
// /views/oj/contest/children/ContestPrint.vue
Print_Title:'比赛文本打印',
Print_tips:'请将需要打印的文本放入内容框内提交。注意:请不要恶意提交!',
Content:'内容',
Content_cannot_be_empty:'内容不能为空',
The_number_of_content_cannot_be_less_than_50:'内容字符数不能低于50',
Success_submit_tips:'提交成功!请耐心等待工作人员打印!',
// /views/oj/contest/children/ContestAdminPrint.vue
Download:'下载',
Printed:'已打印',
Not_Printed:'未打印',
// /views/oj/contest/children/ContestRejudgeAdmin.vue
Contest_Rejudge:'比赛重新测评',
ID: 'ID',

View File

@ -16,6 +16,8 @@ import ContestRank from "@/views/oj/contest/children/ContestRank.vue"
import ACMInfoAdmin from "@/views/oj/contest/children/ACMInfoAdmin.vue"
import Announcements from "@/components/oj/common/Announcements.vue"
import ContestComment from "@/views/oj/contest/children/ContestComment.vue"
import ContestPrint from "@/views/oj/contest/children/ContestPrint.vue"
import ContestAdminPrint from "@/views/oj/contest/children/ContestAdminPrint.vue"
import ContestRejudgeAdmin from "@/views/oj/contest/children/ContestRejudgeAdmin.vue"
import DiscussionList from "@/views/oj/discussion/discussionList.vue"
import Discussion from "@/views/oj/discussion/discussion.vue"
@ -161,6 +163,18 @@ const ojRoutes = [
path:'comment',
component: ContestComment,
meta: { title: 'Contest Comment'}
},
{
name: 'ContestPrint',
path:'print',
component: ContestPrint,
meta: { title: 'Contest Print'}
},
{
name: 'ContestAdminPrint',
path:'admin-print',
component: ContestAdminPrint,
meta: { title: 'Contest Admin Print'}
}
]
},

View File

@ -8,7 +8,8 @@ const state = {
submitAccess:false, // 保护比赛的提交权限
forceUpdate: false,
contest: {
auth: CONTEST_TYPE.PUBLIC
auth: CONTEST_TYPE.PUBLIC,
openPrint: false,
},
contestProblems: [],
itemVisible: {

View File

@ -115,42 +115,106 @@
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item :label="$t('m.Contest_Auth')" required>
<el-select v-model="contest.auth">
<el-option :label="$t('m.Public')" :value="0"></el-option>
<el-option :label="$t('m.Private')" :value="1"></el-option>
<el-option :label="$t('m.Protected')" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item
:label="$t('m.Contest_Password')"
v-show="contest.auth != 0"
:required="contest.auth != 0"
<el-row :gutter="30">
<el-col :md="8" :xs="24">
<el-form-item :label="$t('m.Print_Func')" required>
<el-switch
v-model="contest.openPrint"
:active-text="$t('m.Support_Offline_Print')"
:inactive-text="$t('m.Not_Support_Print')"
>
</el-switch>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="30">
<el-col :md="8" :xs="24">
<el-form-item :label="$t('m.Contest_Auth')" required>
<el-select v-model="contest.auth">
<el-option :label="$t('m.Public')" :value="0"></el-option>
<el-option :label="$t('m.Private')" :value="1"></el-option>
<el-option :label="$t('m.Protected')" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item
:label="$t('m.Contest_Password')"
v-show="contest.auth != 0"
:required="contest.auth != 0"
>
<el-input
v-model="contest.pwd"
:placeholder="$t('m.Contest_Password')"
></el-input>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item
:label="$t('m.Account_Limit')"
v-show="contest.auth != 0"
:required="contest.auth != 0"
>
<el-switch v-model="contest.openAccountLimit"> </el-switch>
</el-form-item>
</el-col>
</el-row>
<template v-if="contest.openAccountLimit">
<el-form :model="formRule">
<el-col :md="6" :xs="24">
<el-form-item :label="$t('m.Prefix')" prop="prefix">
<el-input
v-model="formRule.prefix"
placeholder="Prefix"
></el-input>
</el-form-item>
</el-col>
<el-col :md="6" :xs="24">
<el-form-item :label="$t('m.Suffix')" prop="suffix">
<el-input
v-model="formRule.suffix"
placeholder="Suffix"
></el-input>
</el-form-item>
</el-col>
<el-col :md="6" :xs="24">
<el-form-item :label="$t('m.Start_Number')" prop="number_from">
<el-input-number
v-model="formRule.number_from"
style="width: 100%"
></el-input-number>
</el-form-item>
</el-col>
<el-col :md="6" :xs="24">
<el-form-item :label="$t('m.End_Number')" prop="number_to">
<el-input-number
v-model="formRule.number_to"
style="width: 100%"
></el-input-number>
</el-form-item>
</el-col>
</el-form>
<div
class="userPreview"
v-if="formRule.number_from <= formRule.number_to"
>
<el-input
v-model="contest.pwd"
:placeholder="$t('m.Contest_Password')"
></el-input>
</el-form-item>
</el-col>
<!-- <el-col :span="24">
<el-form-item label="Allowed IP Ranges">
<div v-for="(range, index) in contest.allowed_ip_ranges" :key="index">
<el-row :gutter="20" style="margin-bottom: 15px">
<el-col :span="8">
<el-input v-model="range.value" placeholder="CIDR Network"></el-input>
</el-col>
<el-col :span="10">
<el-button icon="el-icon-plus" @click="addIPRange" type="primary"></el-button>
<el-button icon="el-icon-delete-solid" @click.native="removeIPRange(range)" type="danger"></el-button>
</el-col>
</el-row>
</div>
</el-form-item>
</el-col> -->
{{ $t('m.The_allowed_account_will_be') }}
{{ formRule.prefix + formRule.number_from + formRule.suffix }},
<span v-if="formRule.number_from + 1 < formRule.number_to">
{{
formRule.prefix +
(formRule.number_from + 1) +
formRule.suffix +
'...'
}}
</span>
<span v-if="formRule.number_from + 1 <= formRule.number_to">
{{ formRule.prefix + formRule.number_to + formRule.suffix }}
</span>
</div>
</template>
</el-row>
</el-form>
<el-button type="primary" @click.native="saveContest">{{
@ -189,9 +253,15 @@ export default {
sealRank: true,
sealRankTime: '', //
auth: 0,
// allowed_ip_ranges: [{
// value: ''
// }]
openPrint: false,
openAccountLimit: false,
accountLimitRule: '',
},
formRule: {
prefix: '',
suffix: '',
number_from: 0,
number_to: 10,
},
};
},
@ -224,14 +294,6 @@ export default {
.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})
// }
// if (ranges.length === 0) {
// ranges.push({value: ''})
// }
// data.allowed_ip_ranges = ranges
this.contest = data;
this.changeDuration();
//
@ -243,6 +305,9 @@ export default {
.toString();
let allHour = moment(this.contest.startTime).toString();
let sealRankTime = moment(this.contest.sealRankTime).toString();
this.formRule = this.changeStrToAccountRule(
this.contest.accountLimitRule
);
switch (sealRankTime) {
case halfHour:
this.seal_rank_time = 0;
@ -302,6 +367,12 @@ export default {
return;
}
if (this.contest.openAccountLimit) {
this.contest.accountLimitRule = this.changeAccountRuleToStr(
this.formRule
);
}
let funcName =
this.$route.name === 'admin-edit-contest'
? 'admin_editContest'
@ -324,13 +395,6 @@ export default {
this.contest.sealRankTime = moment(this.contest.startTime);
}
let data = Object.assign({}, this.contest);
// let ranges = []
// for (let v of data.allowed_ip_ranges) {
// if (v.value !== '') {
// ranges.push(v.value)
// }
// }
// data.allowed_ip_ranges = ranges
if (funcName === 'admin_createContest') {
data['uid'] = this.userInfo.uid;
data['author'] = this.userInfo.username;
@ -360,15 +424,41 @@ export default {
this.contest.duration = durationMS;
}
},
// addIPRange () {
// this.contest.allowed_ip_ranges.push({value: ''})
// },
// removeIPRange (range) {
// let index = this.contest.allowed_ip_ranges.indexOf(range)
// if (index !== -1) {
// this.contest.allowed_ip_ranges.splice(index, 1)
// }
// }
changeAccountRuleToStr(formRule) {
let result =
'<prefix>' +
formRule.prefix +
'</prefix><suffix>' +
formRule.suffix +
'</suffix><start>' +
formRule.number_from +
'</start><end>' +
formRule.number_to +
'</end>';
return result;
},
changeStrToAccountRule(value) {
let reg =
'<prefix>([\\s\\S]*?)</prefix><suffix>([\\s\\S]*?)</suffix><start>([\\s\\S]*?)</start><end>([\\s\\S]*?)</end>';
let re = RegExp(reg, 'g');
let tmp = re.exec(value);
return {
prefix: tmp[1],
suffix: tmp[2],
number_from: tmp[3],
number_to: tmp[4],
};
},
},
};
</script>
<style scoped>
.userPreview {
padding-left: 10px;
padding-top: 20px;
padding-bottom: 20px;
color: red;
font-size: 16px;
margin-bottom: 10px;
}
</style>

View File

@ -285,6 +285,7 @@ export default {
let searchParams = {
currentPage: page,
keyword: this.keyword,
admin: true,
};
api.getDiscussionList(this.pageSize, searchParams).then(
(res) => {

View File

@ -172,7 +172,8 @@
<vxe-table-column
:title="$t('m.Username')"
field="username"
min-width="150"
min-width="96"
show-overflow
>
<template v-slot="{ row }">
{{ row[0] }}
@ -182,6 +183,7 @@
:title="$t('m.Password')"
field="password"
min-width="150"
show-overflow
>
<template v-slot="{ row }">
{{ row[1] }}
@ -190,12 +192,23 @@
<vxe-table-column
:title="$t('m.Email')"
field="email"
min-width="150"
min-width="120"
show-overflow
>
<template v-slot="{ row }">
{{ row[2] }}
</template>
</vxe-table-column>
<vxe-table-column
:title="$t('m.RealName')"
field="realname"
min-width="150"
show-overflow
>
<template v-slot="{ row }">
{{ row[3] }}
</template>
</vxe-table-column>
</vxe-table>
<div class="panel-options">
@ -764,7 +777,7 @@ export default {
papa.parse(file, {
complete: (results) => {
let data = results.data.filter((user) => {
return user[0] && user[1] && user[2];
return user[0] && user[1];
});
let delta = results.data.length - data.length;
if (delta > 0) {

View File

@ -182,6 +182,20 @@
</transition>
</el-tab-pane>
<el-tab-pane
name="ContestPrint"
lazy
:disabled="contestMenuDisabled"
v-if="contest.openPrint"
>
<span slot="label"
><i class="el-icon-printer"></i>&nbsp;{{ $t('m.Print') }}</span
>
<transition name="el-zoom-in-bottom">
<router-view v-if="route_name === 'ContestPrint'"></router-view>
</transition>
</el-tab-pane>
<el-tab-pane
name="ContestACInfo"
lazy
@ -198,6 +212,24 @@
</transition>
</el-tab-pane>
<el-tab-pane
name="ContestAdminPrint"
lazy
:disabled="contestMenuDisabled"
v-if="isSuperAdmin && contest.openPrint"
>
<span slot="label"
><i class="el-icon-printer"></i>&nbsp;{{
$t('m.Admin_Print')
}}</span
>
<transition name="el-zoom-in-bottom">
<router-view
v-if="route_name === 'ContestAdminPrint'"
></router-view>
</transition>
</el-tab-pane>
<el-tab-pane
name="ContestRejudgeAdmin"
lazy

View File

@ -87,7 +87,7 @@
}}</el-tag>
</template>
</vxe-table-column>
<vxe-table-column field="option" title="Option" min-width="150">
<vxe-table-column field="option" :title="$t('m.Option')" min-width="150">
<template v-slot="{ row }">
<el-button
type="primary"
@ -109,7 +109,6 @@
</el-card>
</template>
<script>
import { mapState } from 'vuex';
import api from '@/common/api';
import myMessage from '@/common/message';
const Pagination = () => import('@/components/oj/common/Pagination');
@ -121,7 +120,7 @@ export default {
data() {
return {
page: 1,
limit: 30,
limit: 20,
total: 0,
btnLoading: false,
autoRefresh: false,
@ -129,7 +128,6 @@ export default {
};
},
mounted() {
this.contestID = this.$route.params.contestID;
this.getACInfo(1);
},
methods: {
@ -182,11 +180,6 @@ export default {
}
},
},
computed: {
...mapState({
contest: (state) => state.contest.contest,
}),
},
beforeDestroy() {
clearInterval(this.refreshFunc);
},

View File

@ -0,0 +1,195 @@
<template>
<el-card shadow="always">
<div slot="header">
<span class="panel-title">{{ $t('m.Admin_Print') }}</span>
<div class="filter-row">
<span>
{{ $t('m.Auto_Refresh') }}(10s)
<el-switch
@change="handleAutoRefresh"
v-model="autoRefresh"
></el-switch>
</span>
<span>
<el-button
type="primary"
@click="getContestPrint(1)"
size="small"
icon="el-icon-refresh"
:loading="btnLoading"
>{{ $t('m.Refresh') }}</el-button
>
</span>
</div>
</div>
<vxe-table
border="inner"
stripe
auto-resize
align="center"
:data="printList"
>
<vxe-table-column
field="username"
:title="$t('m.Username')"
min-width="150"
>
<template v-slot="{ row }">
<span
><a
@click="getUserTotalSubmit(row.username)"
style="color:rgb(87, 163, 243);"
>{{ row.username }}</a
>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="realname"
:title="$t('m.RealName')"
min-width="150"
></vxe-table-column>
<vxe-table-column
field="gmtCreate"
min-width="150"
:title="$t('m.Submit_Time')"
>
<template v-slot="{ row }">
<span>{{ row.submitTime | localtime }}</span>
</template>
</vxe-table-column>
<vxe-table-column field="status" :title="$t('m.Status')" min-width="150">
<template v-slot="{ row }">
<el-tag effect="dark" color="#19be6b" v-if="row.status == 1">{{
$t('m.Printed')
}}</el-tag>
<el-tag effect="dark" color="#f90" v-if="row.status == 0">{{
$t('m.Not_Printed')
}}</el-tag>
</template>
</vxe-table-column>
<vxe-table-column field="option" :title="$t('m.Option')" min-width="150">
<template v-slot="{ row }">
<el-button
type="primary"
size="small"
icon="el-icon-download"
@click="downloadSubmissions(row.id)"
round
>{{ $t('m.Download') }}</el-button
>
<el-button
type="success"
size="small"
icon="el-icon-circle-check"
@click="updateStatus(row.id)"
round
>{{ $t('m.OK') }}</el-button
>
</template>
</vxe-table-column>
</vxe-table>
<Pagination
:total="total"
:page-size.sync="limit"
:current.sync="page"
@on-change="getContestPrint"
></Pagination>
</el-card>
</template>
<script>
import api from '@/common/api';
import myMessage from '@/common/message';
import utils from '@/common/utils';
const Pagination = () => import('@/components/oj/common/Pagination');
export default {
name: 'Contest-Print-Admin',
components: {
Pagination,
},
data() {
return {
page: 1,
limit: 15,
total: 0,
btnLoading: false,
autoRefresh: false,
contestID: null,
printList: [],
};
},
mounted() {
this.contestID = this.$route.params.contestID;
this.getContestPrint(1);
},
methods: {
updateStatus(id) {
let params = {
id: id,
cid: this.contestID,
};
api.updateContestPrintStatus(params).then((res) => {
myMessage.success(this.$i18n.t('m.Update_Successfully'));
this.getContestPrint(1);
});
},
getContestPrint(page = 1) {
let params = {
cid: this.contestID,
currentPage: page,
limit: this.limit,
};
this.btnLoading = true;
api
.getContestPrintList(params)
.then((res) => {
this.btnLoading = false;
this.printList = res.data.data.records;
this.total = res.data.data.total;
})
.catch(() => {
this.btnLoading = false;
});
},
downloadSubmissions(id) {
let url = `/api/file/download-contest-print-text?id=${id}`;
utils.downloadFile(url);
},
handleAutoRefresh() {
if (this.autoRefresh) {
this.refreshFunc = setInterval(() => {
this.page = 1;
this.getContestPrint(1);
}, 10000);
} else {
clearInterval(this.refreshFunc);
}
},
},
beforeDestroy() {
clearInterval(this.refreshFunc);
},
};
</script>
<style scoped>
.filter-row {
float: right;
}
@media screen and (max-width: 768px) {
.filter-row span {
margin-right: 2px;
}
}
@media screen and (min-width: 768px) {
.filter-row span {
margin-right: 20px;
}
}
/deep/ .el-tag--dark {
border-color: #fff;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<el-card shadow="always">
<div slot="header">
<span class="panel-title">{{ $t('m.Print') }}</span>
</div>
<div class="print-tips">
<el-alert
:title="$t('m.Print_Title')"
type="warning"
:closable="false"
center
:description="$t('m.Print_tips')"
show-icon
>
</el-alert>
</div>
<el-form
:model="ruleForm"
:rules="rules"
ref="ruleForm"
label-width="100px"
class="demo-ruleForm"
>
<el-form-item :label="$t('m.Content')" prop="content">
<el-input
type="textarea"
v-model="ruleForm.content"
:rows="20"
></el-input>
</el-form-item>
<el-form-item style="text-align: center;">
<el-button type="primary" @click="onSubmit">{{
$t('m.Submit')
}}</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script>
import mMessage from '@/common/message';
import api from '@/common/api';
export default {
data() {
return {
ruleForm: {
content: '',
},
rules: {
content: [{ required: true, trigger: 'blur' }],
},
contestID: null,
};
},
mounted() {
this.contestID = this.$route.params.contestID;
},
methods: {
onSubmit() {
if (!this.ruleForm.content) {
mMessage.error(this.$i18n.t('m.Content_cannot_be_empty'));
return;
}
if (this.ruleForm.content.length < 50) {
mMessage.error(
this.$i18n.t('m.The_number_of_content_cannot_be_less_than_50')
);
return;
}
let data = {
cid: this.contestID,
content: this.ruleForm.content,
};
api.submitPrintText(data).then((res) => {
this.$confirm(
this.$i18n.t('m.Success_submit_tips'),
this.$i18n.t('m.Submit_code_successfully'),
{
type: 'success',
center: true,
confirmButtonText: this.$i18n.t('m.OK'),
}
);
});
},
},
};
</script>
<style scoped>
.print-tips {
margin-left: 50px;
padding: 30px;
padding-top: 0px;
}
</style>

View File

@ -401,7 +401,7 @@ export default {
data() {
return {
total: 0,
limit: 15,
limit: 10,
currentPage: 1,
showEditDiscussionDialog: false,
discussion: {

View File

@ -4,208 +4,387 @@
<!--problem main-->
<el-row class="problem-box">
<el-col :sm="24" :md="24" :lg="12" class="problem-left">
<el-card :padding="10" shadow class="problem-detail">
<div slot="header" class="panel-title">
<span>{{ problemData.problem.title }}</span
><br />
<span v-if="contestID && !contestEnded"
><el-tag effect="plain" size="small">{{
$t('m.Contest_Problem')
}}</el-tag></span
<el-tabs
v-model="activeName"
type="border-card"
@tab-click="handleClickTab"
>
<el-tab-pane name="problemDetail">
<span slot="label"
><i class="fa fa-list-alt"></i>
{{ $t('m.Problem_Description') }}</span
>
<div v-else-if="problemData.tags.length > 0" class="problem-tag">
<el-popover placement="right-start" width="60" trigger="hover">
<el-tag
slot="reference"
size="small"
type="primary"
style="cursor: pointer;"
effect="plain"
>{{ $t('m.Show_Tags') }}</el-tag
<div :padding="10" shadow class="problem-detail">
<div slot="header" class="panel-title">
<span>{{ problemData.problem.title }}</span
><br />
<span v-if="contestID && !contestEnded"
><el-tag effect="plain" size="small">{{
$t('m.Contest_Problem')
}}</el-tag></span
>
<el-tag
v-for="tag in problemData.tags"
:key="tag"
effect="plain"
size="small"
style="margin-right:5px;margin-top:2px"
>{{ tag }}</el-tag
<div
v-else-if="problemData.tags.length > 0"
class="problem-tag"
>
</el-popover>
</div>
<div v-else-if="problemData.tags.length == 0" class="problem-tag">
<el-tag effect="plain" size="small">{{
$t('m.No_tag')
}}</el-tag>
</div>
<div class="problem-menu">
<span v-if="!contestID">
<el-link
type="primary"
:underline="false"
@click="goProblemDiscussion"
><i class="fa fa-comments" aria-hidden="true"></i>
{{ $t('m.Problem_Discussion') }}</el-link
></span
>
<span>
<el-link
type="primary"
:underline="false"
@click="graphVisible = !graphVisible"
><i class="fa fa-pie-chart" aria-hidden="true"></i>
{{ $t('m.Statistic') }}</el-link
></span
>
<span>
<el-link
type="primary"
:underline="false"
@click="goProblemSubmission"
><i class="fa fa-bars" aria-hidden="true"></i>
{{ $t('m.Solution') }}</el-link
></span
>
</div>
<div class="question-intr">
<template v-if="!isCFProblem">
<span
>{{ $t('m.Time_Limit') }}C/C++
{{ problemData.problem.timeLimit }}MS{{ $t('m.Other') }}
{{ problemData.problem.timeLimit * 2 }}MS</span
><br />
<span
>{{ $t('m.Memory_Limit') }}C/C++
{{ problemData.problem.memoryLimit }}MB{{
$t('m.Other')
}}
{{ problemData.problem.memoryLimit * 2 }}MB</span
><br />
</template>
<template v-else>
<span
>{{ $t('m.Time_Limit') }}{{
problemData.problem.timeLimit
}}MS</span
>
<br />
<span
>{{ $t('m.Memory_Limit') }}{{
problemData.problem.memoryLimit
}}MB</span
><br />
</template>
<span
>{{ $t('m.Level') }}{{
PROBLEM_LEVEL[problemData.problem.difficulty]['name']
}}</span
>
<br />
<template v-if="problemData.problem.type == 1">
<span
>{{ $t('m.Score') }}{{ problemData.problem.ioScore }}
</span>
<span v-if="!contestID" style="margin-left:5px;">
{{ $t('m.OI_Rank_Score') }}{{
calcOIRankScore(
problemData.problem.ioScore,
problemData.problem.difficulty
)
}}(0.1*{{ $t('m.Score') }}+2*{{ $t('m.Level') }})
</span>
<br />
</template>
<template v-if="problemData.problem.author">
<span
>{{ $t('m.Created') }}{{
problemData.problem.author
}}</span
><br />
</template>
</div>
</div>
<div id="problem-content">
<p class="title">{{ $t('m.Description') }}</p>
<p
class="content markdown-body"
v-html="problemData.problem.description"
v-katex
v-highlight
></p>
<p class="title">{{ $t('m.Input') }}</p>
<p
class="content markdown-body"
v-html="problemData.problem.input"
v-katex
v-highlight
></p>
<p class="title">{{ $t('m.Output') }}</p>
<p
class="content markdown-body"
v-html="problemData.problem.output"
v-katex
v-highlight
></p>
<div
v-for="(example, index) of problemData.problem.examples"
:key="index"
>
<div class="flex-container example">
<div class="example-input">
<p class="title">
{{ $t('m.Sample_Input') }} {{ index + 1 }}
<a
class="copy"
v-clipboard:copy="example.input"
v-clipboard:success="onCopy"
v-clipboard:error="onCopyError"
<el-popover
placement="right-start"
width="60"
trigger="hover"
>
<el-tag
slot="reference"
size="small"
type="primary"
style="cursor: pointer;"
effect="plain"
>{{ $t('m.Show_Tags') }}</el-tag
>
<i class="el-icon-document-copy"></i>
</a>
</p>
<pre>{{ example.input }}</pre>
<el-tag
v-for="tag in problemData.tags"
:key="tag"
effect="plain"
size="small"
style="margin-right:5px;margin-top:2px"
>{{ tag }}</el-tag
>
</el-popover>
</div>
<div class="example-output">
<p class="title">
{{ $t('m.Sample_Output') }} {{ index + 1 }}
<a
class="copy"
v-clipboard:copy="example.output"
v-clipboard:success="onCopy"
v-clipboard:error="onCopyError"
<div
v-else-if="problemData.tags.length == 0"
class="problem-tag"
>
<el-tag effect="plain" size="small">{{
$t('m.No_tag')
}}</el-tag>
</div>
<div class="problem-menu">
<span v-if="!contestID">
<el-link
type="primary"
:underline="false"
@click="goProblemDiscussion"
><i class="fa fa-comments" aria-hidden="true"></i>
{{ $t('m.Problem_Discussion') }}</el-link
></span
>
<span>
<el-link
type="primary"
:underline="false"
@click="graphVisible = !graphVisible"
><i class="fa fa-pie-chart" aria-hidden="true"></i>
{{ $t('m.Statistic') }}</el-link
></span
>
<span>
<el-link
type="primary"
:underline="false"
@click="goProblemSubmission"
><i class="fa fa-bars" aria-hidden="true"></i>
{{ $t('m.Solutions') }}</el-link
></span
>
</div>
<div class="question-intr">
<template v-if="!isCFProblem">
<span
>{{ $t('m.Time_Limit') }}C/C++
{{ problemData.problem.timeLimit }}MS{{
$t('m.Other')
}}
{{ problemData.problem.timeLimit * 2 }}MS</span
><br />
<span
>{{ $t('m.Memory_Limit') }}C/C++
{{ problemData.problem.memoryLimit }}MB{{
$t('m.Other')
}}
{{ problemData.problem.memoryLimit * 2 }}MB</span
><br />
</template>
<template v-else>
<span
>{{ $t('m.Time_Limit') }}{{
problemData.problem.timeLimit
}}MS</span
>
<i class="el-icon-document-copy"></i>
</a>
</p>
<pre>{{ example.output }}</pre>
<br />
<span
>{{ $t('m.Memory_Limit') }}{{
problemData.problem.memoryLimit
}}MB</span
><br />
</template>
<span
>{{ $t('m.Level') }}{{
PROBLEM_LEVEL[problemData.problem.difficulty]['name']
}}</span
>
<br />
<template v-if="problemData.problem.type == 1">
<span
>{{ $t('m.Score') }}{{ problemData.problem.ioScore }}
</span>
<span v-if="!contestID" style="margin-left:5px;">
{{ $t('m.OI_Rank_Score') }}{{
calcOIRankScore(
problemData.problem.ioScore,
problemData.problem.difficulty
)
}}(0.1*{{ $t('m.Score') }}+2*{{ $t('m.Level') }})
</span>
<br />
</template>
<template v-if="problemData.problem.author">
<span
>{{ $t('m.Created') }}{{
problemData.problem.author
}}</span
><br />
</template>
</div>
</div>
</div>
<template v-if="problemData.problem.hint">
<p class="title">{{ $t('m.Hint') }}</p>
<el-card dis-hover>
<div id="problem-content">
<p class="title">{{ $t('m.Description') }}</p>
<p
class="hint-content markdown-body"
v-html="problemData.problem.hint"
class="content markdown-body"
v-html="problemData.problem.description"
v-katex
v-highlight
></p>
<p class="title">{{ $t('m.Input') }}</p>
<p
class="content markdown-body"
v-html="problemData.problem.input"
v-katex
v-highlight
></p>
</el-card>
</template>
<template v-if="problemData.problem.source && !contestID">
<p class="title">{{ $t('m.Source') }}</p>
<p class="content" v-html="problemData.problem.source"></p>
<p class="title">{{ $t('m.Output') }}</p>
<p
class="content markdown-body"
v-html="problemData.problem.output"
v-katex
v-highlight
></p>
<div
v-for="(example, index) of problemData.problem.examples"
:key="index"
>
<div class="flex-container example">
<div class="example-input">
<p class="title">
{{ $t('m.Sample_Input') }} {{ index + 1 }}
<a
class="copy"
v-clipboard:copy="example.input"
v-clipboard:success="onCopy"
v-clipboard:error="onCopyError"
>
<i class="el-icon-document-copy"></i>
</a>
</p>
<pre>{{ example.input }}</pre>
</div>
<div class="example-output">
<p class="title">
{{ $t('m.Sample_Output') }} {{ index + 1 }}
<a
class="copy"
v-clipboard:copy="example.output"
v-clipboard:success="onCopy"
v-clipboard:error="onCopyError"
>
<i class="el-icon-document-copy"></i>
</a>
</p>
<pre>{{ example.output }}</pre>
</div>
</div>
</div>
<template v-if="problemData.problem.hint">
<p class="title">{{ $t('m.Hint') }}</p>
<el-card dis-hover>
<p
class="hint-content markdown-body"
v-html="problemData.problem.hint"
v-katex
v-highlight
></p>
</el-card>
</template>
<template v-if="problemData.problem.source && !contestID">
<p class="title">{{ $t('m.Source') }}</p>
<p class="content" v-html="problemData.problem.source"></p>
</template>
</div>
</div>
</el-tab-pane>
<el-tab-pane name="mySubmission">
<span slot="label"
><i class="el-icon-time"></i> {{ $t('m.My_Submission') }}</span
>
<template v-if="!isAuthenticated">
<div style="margin:50px 0px;margin-left:-20px;">
<el-alert
:title="$t('m.Please_login_first')"
type="warning"
center
:closable="false"
:description="$t('m.Login_to_view_your_submission_history')"
show-icon
>
</el-alert>
</div>
</template>
</div>
</el-card>
<template v-else>
<div style="margin:20px 0px;margin-right:10px;">
<vxe-table
align="center"
:data="mySubmissions"
stripe
auto-resize
border="inner"
:loading="loadingTable"
>
<vxe-table-column
:title="$t('m.Submit_Time')"
min-width="96"
>
<template v-slot="{ row }">
<span
><el-tooltip
:content="row.submitTime | localtime"
placement="top"
>
<span>{{ row.submitTime | fromNow }}</span>
</el-tooltip></span
>
</template>
</vxe-table-column>
<vxe-table-column
field="status"
:title="$t('m.Status')"
min-width="160"
>
<template v-slot="{ row }">
<span :class="getStatusColor(row.status)">{{
JUDGE_STATUS[row.status].name
}}</span>
</template>
</vxe-table-column>
<vxe-table-column :title="$t('m.Time')" min-width="96">
<template v-slot="{ row }">
<span>{{ submissionTimeFormat(row.time) }}</span>
</template>
</vxe-table-column>
<vxe-table-column :title="$t('m.Memory')" min-width="96">
<template v-slot="{ row }">
<span>{{ submissionMemoryFormat(row.memory) }}</span>
</template>
</vxe-table-column>
<vxe-table-column
:title="$t('m.Score')"
min-width="64"
v-if="problemData.problem.type == 1"
>
<template v-slot="{ row }">
<template v-if="contestID && row.score != null">
<el-tag
effect="plain"
size="medium"
:type="JUDGE_STATUS[row.status]['type']"
>{{ row.score }}</el-tag
>
</template>
<template v-else-if="row.score != null">
<el-tooltip placement="top">
<div slot="content">
{{ $t('m.Problem_Score') }}{{
row.score != null ? row.score : $t('m.Unknown')
}}<br />{{ $t('m.OI_Rank_Score') }}{{
row.oiRankScore != null
? row.oiRankScore
: $t('m.Unknown')
}}<br />
{{
$t('m.OI_Rank_Calculation_Rule')
}}(score*0.1+difficulty*2)*(ac_cases/sum_cases)
</div>
<el-tag
effect="plain"
size="medium"
:type="JUDGE_STATUS[row.status]['type']"
>{{ row.score }}</el-tag
>
</el-tooltip>
</template>
<template
v-else-if="
row.status == JUDGE_STATUS_RESERVE['Pending'] ||
row.status == JUDGE_STATUS_RESERVE['Compiling'] ||
row.status == JUDGE_STATUS_RESERVE['Judging']
"
>
<el-tag
effect="plain"
size="medium"
:type="JUDGE_STATUS[row.status]['type']"
>
<i class="el-icon-loading"></i>
</el-tag>
</template>
<template v-else>
<el-tag
effect="plain"
size="medium"
:type="JUDGE_STATUS[row.status]['type']"
>--</el-tag
>
</template>
</template>
</vxe-table-column>
<vxe-table-column
field="language"
:title="$t('m.Language')"
show-overflow
min-width="130"
>
<template v-slot="{ row }">
<el-tooltip
class="item"
effect="dark"
:content="$t('m.View_submission_details')"
placement="top"
>
<el-button
type="text"
@click="showSubmitDetail(row)"
>{{ row.language }}</el-button
>
</el-tooltip>
</template>
</vxe-table-column>
</vxe-table>
<Pagination
:total="mySubmission_total"
:page-size="mySubmission_limit"
@on-change="getMySubmission"
:current.sync="mySubmission_currentPage"
></Pagination>
</div>
</template>
</el-tab-pane>
</el-tabs>
</el-col>
<div
class="problem-resize hidden-md-and-down"
@ -452,6 +631,7 @@ import api from '@/common/api';
import myMessage from '@/common/message';
import { addCodeBtn } from '@/common/codeblock';
const CodeMirror = () => import('@/components/oj/common/CodeMirror.vue');
import Pagination from '@/components/oj/common/Pagination';
//
const filtedStatus = ['wa', 'ce', 'ac', 'pa', 'tle', 'mle', 're', 'pe'];
@ -459,6 +639,7 @@ export default {
name: 'ProblemDetails',
components: {
CodeMirror,
Pagination,
},
data() {
return {
@ -500,10 +681,17 @@ export default {
height: '380',
},
JUDGE_STATUS_RESERVE: {},
JUDGE_STATUS: {},
PROBLEM_LEVEL: {},
RULE_TYPE: {},
toResetWatch: false,
toWatchProblem: false,
activeName: 'problemDetail',
loadingTable: false,
mySubmission_total: 0,
mySubmission_limit: 15,
mySubmission_currentPage: 1,
mySubmissions: [],
};
},
//
@ -524,6 +712,7 @@ export default {
created() {
this.JUDGE_STATUS_RESERVE = Object.assign({}, JUDGE_STATUS_RESERVE);
this.JUDGE_STATUS = Object.assign({}, JUDGE_STATUS);
this.PROBLEM_LEVEL = Object.assign({}, PROBLEM_LEVEL);
this.RULE_TYPE = Object.assign({}, RULE_TYPE);
},
@ -533,6 +722,76 @@ export default {
},
methods: {
...mapActions(['changeDomTitle']),
handleClickTab({ name }) {
if (name == 'mySubmission') {
this.getMySubmission();
}
},
getMySubmission() {
let params = {
onlyMine: true,
currentPage: this.mySubmission_currentPage,
problemID: this.problemID,
contestID: this.contestID,
limit: this.mySubmission_limit,
};
if (this.contestID) {
if (this.contestStatus == CONTEST_STATUS.SCHEDULED) {
params.beforeContestSubmit = true;
} else {
params.beforeContestSubmit = false;
}
}
let func = this.contestID
? 'getContestSubmissionList'
: 'getSubmissionList';
this.loadingTable = true;
api[func](this.mySubmission_limit, utils.filterEmptyValue(params))
.then(
(res) => {
let data = res.data.data;
this.mySubmissions = data.records;
this.mySubmission_total = data.total;
this.loadingTable = false;
},
(err) => {
this.loadingTable = false;
}
)
.catch(() => {
this.loadingTable = false;
});
},
getStatusColor(status) {
return 'el-tag el-tag--medium status-' + JUDGE_STATUS[status].color;
},
submissionTimeFormat(time) {
return utils.submissionTimeFormat(time);
},
submissionMemoryFormat(memory) {
return utils.submissionMemoryFormat(memory);
},
showSubmitDetail(row) {
if (row.cid != 0) {
//
this.$router.push({
name: 'ContestSubmissionDeatil',
params: {
contestID: this.$route.params.contestID,
problemID: row.displayId,
submitID: row.submitId,
},
});
} else {
this.$router.push({
name: 'SubmissionDeatil',
params: { submitID: row.submitId },
});
}
},
dragControllerDiv() {
var resize = document.getElementsByClassName('problem-resize');
var left = document.getElementsByClassName('problem-left');
@ -1089,13 +1348,20 @@ a {
height: 100%;
}
/deep/.el-tabs--border-card > .el-tabs__content {
padding-top: 0px;
padding-right: 0px;
}
.problem-detail {
padding-right: 15px;
}
@media screen and (min-width: 768px) {
.problem-detail {
height: 700px !important;
overflow-y: auto;
}
.submit-detail {
height: 700px !important;
height: 755px !important;
overflow-y: auto;
}
.problem-tag {
@ -1205,7 +1471,7 @@ a {
}
#problem-content {
margin-top: -50px;
margin-top: -40px;
}
#problem-content .title {
font-size: 16px;

View File

@ -97,19 +97,24 @@
v-if="isIOProblem"
>
<template v-slot="{ row }">
<el-tooltip placement="top">
<div slot="content">
{{ $t('m.Problem_Score') }}{{
row.score != null ? row.score : $t('m.Unknown')
}}<br />{{ $t('m.OI_Rank_Score') }}{{
row.oiRankScore != null ? row.oiRankScore : $t('m.Unknown')
}}<br />
{{
$t('m.OI_Rank_Calculation_Rule')
}}:(score*0.1+diffculty*2)*(ac_testcase/sum_testcase)
</div>
<span>{{ row.score }}</span>
</el-tooltip>
<template v-if="row.score != null">
<el-tooltip placement="top">
<div slot="content">
{{ $t('m.Problem_Score') }}{{
row.score != null ? row.score : $t('m.Unknown')
}}<br />{{ $t('m.OI_Rank_Score') }}{{
row.oiRankScore != null ? row.oiRankScore : $t('m.Unknown')
}}<br />
{{
$t('m.OI_Rank_Calculation_Rule')
}}:(score*0.1+diffculty*2)*(ac_testcase/sum_testcase)
</div>
<span>{{ row.score }}</span>
</el-tooltip>
</template>
<template v-else>
<span>--</span>
</template>
</template>
</vxe-table-column>
<vxe-table-column :title="$t('m.Length')" min-width="80">