训练模块80%更新,优化比赛排行榜

This commit is contained in:
Himit_ZH 2021-12-06 21:04:45 +08:00
parent e358a78a8c
commit 85c2f96377
66 changed files with 3782 additions and 419 deletions

View File

@ -22,24 +22,32 @@ module.exports = {
title: '开始介绍',
collapsable: true,
children: [
'introducition/',
'introducition/about',
'introducition/'
]
},
{
title: '部署文档',
title: '快速部署',
collapsable: true,
children: [
'deploy/',
'deploy/docker',
'deploy/mysql',
'deploy/mysql-checker',
'deploy/redis',
'deploy/nacos',
'deploy/backend',
'deploy/judgeserver',
'deploy/frontend',
'deploy/rsync'
'deploy/open-https',
'deploy/multi-judgeserver',
'deploy/update'
]
},
{
title: '单体部署',
collapsable: true,
children: [
'monomer/mysql',
'monomer/mysql-checker',
'monomer/redis',
'monomer/nacos',
'monomer/backend',
'monomer/judgeserver',
'monomer/frontend',
'monomer/rsync'
]
},
{
@ -63,8 +71,8 @@ module.exports = {
'use/admin-user',
'use/notice-announcement',
'use/discussion-admin',
'use/multi-judgeserver',
'use/update-fe',
'use/close-free-cdn',
'use/spj'
]
},

View File

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

View File

@ -282,123 +282,3 @@ Password: 开启SMTP服务后生成的随机授权码
> 提示需要开启多台判题机就如当前第4步的操作一样在每台服务器上执行以上的操作即可。
5. 两个服务都启动完成在浏览器输入主服务ip或域名进行访问登录root账号到后台查看服务状态。
## 三、开启Https
- 单机部署:
提供server.crt和server.key证书与密钥文件放置`/standAlone`目录下,与`docker-compose.yml`和`.env`文件放置同一位置,然后修改`docker-compose.yml`中的hoj-frontend的配置
- 分布式部署:
提供server.crt和server.key证书与密钥文件放置`/distributed/main目录下与`docker-compose.yml`和`.env`文件放置同一位置,然后修改`docker-compose.yml`中的hoj-frontend的配置
```yaml
hoj-frontend:
image: registry.cn-shenzhen.aliyuncs.com/hcode/hoj_frontend
container_name: hoj-frontend
restart: always
# 开启https请提供证书
volumes:
- ./server.crt:/etc/nginx/etc/crt/server.crt
- ./server.key:/etc/nginx/etc/crt/server.key
# 修改前端logo
# - ./logo.a0924d7d.png:/usr/share/nginx/html/assets/img/logo.a0924d7d.png
# - ./backstage.8bce8c6e.png:/usr/share/nginx/html/assets/img/backstage.8bce8c6e.png
environment:
- SERVER_NAME=localhost # 提供你的域名!!!!
- BACKEND_SERVER_HOST=${BACKEND_HOST:-172.20.0.5} # backend后端服务地址
- BACKEND_SERVER_PORT=${BACKEND_PORT:-6688} # backend后端服务端口号
- USE_HTTPS=true # 使用https请设置为true
ports:
- "80:80"
- "443:443"
networks:
hoj-network:
ipv4_address: 172.20.0.6
```
## 四、更新最新版本
> 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 pull
docker-compose up -d
```

View File

@ -0,0 +1,35 @@
# 开启HTTPS
- 单机部署:
提供server.crt和server.key证书与密钥文件放置`/standAlone`目录下,与`docker-compose.yml`和`.env`文件放置同一位置,然后修改`docker-compose.yml`中的hoj-frontend的配置
- 分布式部署:
提供server.crt和server.key证书与密钥文件放置`/distributed/main`目录下,与`docker-compose.yml`和`.env`文件放置同一位置,然后修改`docker-compose.yml`中的hoj-frontend的配置
```yaml
hoj-frontend:
image: registry.cn-shenzhen.aliyuncs.com/hcode/hoj_frontend
container_name: hoj-frontend
restart: always
# 开启https请提供证书
volumes:
- ./server.crt:/etc/nginx/etc/crt/server.crt
- ./server.key:/etc/nginx/etc/crt/server.key
# 修改前端logo
# - ./logo.a0924d7d.png:/usr/share/nginx/html/assets/img/logo.a0924d7d.png
# - ./backstage.8bce8c6e.png:/usr/share/nginx/html/assets/img/backstage.8bce8c6e.png
environment:
- SERVER_NAME=localhost # 提供你的域名!!!!
- BACKEND_SERVER_HOST=${BACKEND_HOST:-172.20.0.5} # backend后端服务地址
- BACKEND_SERVER_PORT=${BACKEND_PORT:-6688} # backend后端服务端口号
- USE_HTTPS=true # 使用https请设置为true
ports:
- "80:80"
- "443:443"
networks:
hoj-network:
ipv4_address: 172.20.0.6
```

View File

@ -0,0 +1,99 @@
# 如何更新
## 一、无二次开发的更新
> 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 pull
docker-compose up -d
```
**注意**此次修改成功后以后更新都请在对应的docker-compose.yml当前文件夹下执行`docker-compose pull`拉取最新镜像,然后重新`docker-compose up -d`即可。
## 二、自定义前端的更新
> 附加:如何自定义前端请看这里 => [自定义前端文档](/use/update-fe.html)
1首先到`./hoj/hoj-vue`文件夹中,拉取[hoj-vue](https://gitee.com/himitzh0730/hoj/tree/master/hoj-vue)仓库最新的代码,可能会覆盖本地的修改,请注意合并分支。
```shell
git pull
```
或者重新直接download成zip包然后重新自定义修改前端
2接着重新用npm打包在`./hoj/hoj-vue/dist`文件夹会生成静态的前端文件,放到原来指定的位置即可
```shell
npm run build
```
3其它模块的更新都请在对应的docker-compose.yml当前文件夹下执行`docker-compose pull`拉取最新镜像,然后重新`docker-compose up -d`即可。

View File

@ -203,6 +203,7 @@ services:
- "0.0.0.0:8088:8088"
# - "0.0.0.0:5050:5050" # 一般不开放安全沙盒端口
privileged: true # 设置容器的权限为root
shm_size: 512mb # docker默认的共享内存区域太小设置为512M
```

View File

@ -0,0 +1,195 @@
# 取消前端免费CDN
由于有的机房的网络不支持一些域名的访问有防火墙挡住所以可能前端页面的js和css的CDN访问不了导致页面打不开。
hoj挂载了一些前端库的免费CDN全部都是该域名`cdn.jsdelivr.net`下的免费CDN
可以在对应的电脑浏览器上打开以下链接,如果能正常访问则没有问题
```html
https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js
```
如果有问题,有两种办法解决:
> 前提hoj前端文件不挂载CDN最终打包生成的文件夹大小约8MB
- 如果本身hoj部署在**学校内网机器**上或者**云服务器是无带宽上限、按流量计费的实例**那么可以不用考虑带宽问题可以直接取消CDN挂载直接全部自己打包成对应的静态文件然后挂载到docker的`hoj-frontend`镜像里面,操作如下:
1. 下载前端源代码:[https://gitee.com/himitzh0730/hoj/tree/master/hoj-vue](https://gitee.com/himitzh0730/hoj/tree/master/hoj-vue)
2. 进入`hoj-vue`文件夹,编辑`vue.config.js`文件,按下面的修改
```js
// 该变量改成false
const isProduction = false;
// 本地环境是否需要使用cdn该变量改成false
const devNeedCdn = false;
// 找到下面对应的cdn的js链接和css链接全部注释掉
css: [
// 'https://cdn.jsdelivr.net/npm/element-ui@2.14.0/lib/theme-chalk/index.css',
// "https://cdn.jsdelivr.net/npm/github-markdown-css@4.0.0/github-markdown.min.css",
// "https://cdn.jsdelivr.net/npm/vxe-table@2.9.26/lib/style.css",
],
js: [
// "https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js",
// "https://cdn.jsdelivr.net/npm/vue-router@3.2.0",
// "https://cdn.jsdelivr.net/npm/axios@0.21.0",
// "https://cdn.jsdelivr.net/npm/vuex@3.5.1",
// "https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/index.js",
// "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.3.2/build/highlight.min.js",
// "https://cdn.jsdelivr.net/npm/xe-utils",
// "https://cdn.jsdelivr.net/npm/vxe-table@2.9.26",
// "https://cdn.jsdelivr.net/npm/moment@2.29.1/min/moment.min.js",
// "https://cdn.jsdelivr.net/npm/moment@2.29.1/locale/zh-cn.min.js",
// "https://cdn.jsdelivr.net/npm/moment@2.29.1/locale/en-gb.min.js",
// "https://cdn.jsdelivr.net/npm/echarts@4.9.0/dist/echarts.min.js",
// "https://cdn.jsdelivr.net/npm/vue-echarts@5.0.0-beta.0/dist/vue-echarts.js",
// "https://unpkg.com/mavon-editor@2.9.1/dist/mavon-editor.js"
]
```
3. 进入`hoj-vue/src`文件夹,编辑`main.js`文件,将内容替换成如下:
```js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import Element from 'element-ui'
import i18n from '@/i18n'
import "element-ui/lib/theme-chalk/index.css"
import 'font-awesome/css/font-awesome.min.css'
import Message from 'vue-m-message'
import 'vue-m-message/dist/index.css'
import axios from 'axios'
import Md_Katex from '@iktakahiro/markdown-it-katex'
import 'xe-utils'
import VXETable from 'vxe-table'
import 'vxe-table/lib/style.css'
import Katex from '@/common/katex'
import VueClipboard from 'vue-clipboard2'
import highlight from '@/common/highlight'
import filters from '@/common/filters.js'
import VueCropper from 'vue-cropper'
import ECharts from 'vue-echarts/components/ECharts.vue'
import 'echarts/lib/chart/bar'
import 'echarts/lib/chart/line'
import 'echarts/lib/chart/pie'
import 'echarts/lib/component/title'
import 'echarts/lib/component/grid'
import 'echarts/lib/component/dataZoom'
import 'echarts/lib/component/legend'
import 'echarts/lib/component/tooltip'
import 'echarts/lib/component/toolbox'
import 'echarts/lib/component/markPoint'
Vue.component('ECharts', ECharts)
import VueECharts from 'vue-echarts';
Vue.component('ECharts', VueECharts)
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';
Vue.use(mavonEditor)
import {Drawer,List,Menu,Icon,AppBar,Button,Divider} from 'muse-ui';
import 'muse-ui/dist/muse-ui.css';
import router from './router'
Vue.use(Drawer)
Vue.use(List)
Vue.use(Menu)
Vue.use(Icon)
Vue.use(AppBar)
Vue.use(Button)
Vue.use(Divider)
Object.keys(filters).forEach(key => { // 注册全局过滤器
Vue.filter(key, filters[key])
})
Vue.use(VueParticles) // 粒子特效背景
Vue.use(Katex) // 数学公式渲染
Vue.use(VXETable) // 表格组件
Vue.use(VueClipboard) // 剪贴板
Vue.use(highlight) // 代码高亮
Vue.use(Element,{
i18n: (key, value) => i18n.t(key, value)
})
Vue.use(VueCropper) // 图像剪切
Vue.use(Message, { name: 'msg' }) // `Vue.prototype.$msg` 全局消息提示
Vue.use(SlideVerify) // 滑动验证码组件
Vue.prototype.$axios = axios
Vue.prototype.$markDown = mavonEditor.markdownIt.use(Md_Katex) // 挂载到vue
Vue.config.productionTip = false
new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount('#app')
```
4. 然后使用在`hoj-vue`目录下,使用`npm run build`npm请自行百度下载安装之后会生成一个dist文件夹结构如下
```
dist
├── index.html
├── favicon.ico
└── assets
├── css
│ ├── ....
├── fonts
│ ├── ....
├── img
│ ├── ....
├── js
│ ├── ....
....
....
```
`dist` 文件夹复制到服务器上某个目录下,比如 `/hoj/www/html/dist`,然后修改 `docker-compose.yml`,在 `hoj-frontend` 模块中的 `volumes` 中增加一行 `- /hoj/www/html/dist:/usr/share/nginx/html` (冒号前面的请修改为实际的路径),然后 `docker-compose up -d` 即可。
- 如果云服务器是只有固定小流量出口带宽的例如1M,2M的害怕访问速度太慢但是有钱买CDN服务器可以先按照上面的方式生成对应的本地静态文件夹然后把`dist/assets`文件夹放在CDN服务器上然后修改`dist/index.html`
**(建议有弄过CDN的可以这样搞)**
添加css等文件的导入
```html
<link href="cdn服务器的地址/assets/css/文件名称.css" rel="prefetch">
```
添加js等文件的导入
```html
<script src="cdn服务器的地址/assets/js/文件名称.js">
```
..............................
`dist` 文件夹复制到服务器上某个目录下,比如 `/hoj/www/html/dist`,然后修改 `docker-compose.yml`,在 `hoj-frontend` 模块中的 `volumes` 中增加一行 `- /hoj/www/html/dist:/usr/share/nginx/html` (冒号前面的请修改为实际的路径),然后 `docker-compose up -d` 即可。

View File

@ -21,6 +21,7 @@ import top.hcode.hoj.pojo.dto.ProblemDto;
import top.hcode.hoj.pojo.entity.contest.Contest;
import top.hcode.hoj.pojo.entity.contest.ContestAnnouncement;
import top.hcode.hoj.pojo.entity.contest.ContestProblem;
import top.hcode.hoj.pojo.entity.judge.Judge;
import top.hcode.hoj.pojo.entity.problem.Problem;
import top.hcode.hoj.pojo.vo.AnnouncementVo;
import top.hcode.hoj.pojo.vo.UserRolesVo;
@ -28,6 +29,7 @@ import top.hcode.hoj.service.common.impl.AnnouncementServiceImpl;
import top.hcode.hoj.service.contest.impl.ContestAnnouncementServiceImpl;
import top.hcode.hoj.service.contest.impl.ContestProblemServiceImpl;
import top.hcode.hoj.service.contest.impl.ContestServiceImpl;
import top.hcode.hoj.service.judge.impl.JudgeServiceImpl;
import top.hcode.hoj.service.problem.impl.ProblemServiceImpl;
import top.hcode.hoj.utils.Constants;
@ -64,6 +66,9 @@ public class AdminContestController {
@Autowired
private AnnouncementServiceImpl announcementService;
@Autowired
private JudgeServiceImpl judgeService;
@GetMapping("/get-contest-list")
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
@ -285,31 +290,31 @@ public class AdminContestController {
@DeleteMapping("/problem")
@RequiresAuthentication
@RequiresRoles(value = {"root", "problem_admin"}, logical = Logical.OR)
@Transactional(rollbackFor = Exception.class)
public CommonResult deleteProblem(@RequestParam("pid") Long pid,
@RequestParam(value = "cid", required = false) Long cid) {
boolean result = false;
// 比赛id不为null表示就是从比赛列表移除而已
if (cid != null) {
QueryWrapper<ContestProblem> contestProblemQueryWrapper = new QueryWrapper<>();
contestProblemQueryWrapper.eq("cid", cid).eq("pid", pid);
result = contestProblemService.remove(contestProblemQueryWrapper);
contestProblemService.remove(contestProblemQueryWrapper);
// 把该题目在比赛的提交全部删掉
UpdateWrapper<Judge> judgeUpdateWrapper = new UpdateWrapper<>();
judgeUpdateWrapper.eq("cid", cid).eq("pid", pid);
judgeService.remove(judgeUpdateWrapper);
} else {
/*
problem的id为其他表的外键的表中的对应数据都会被一起删除
*/
result = problemService.removeById(pid);
problemService.removeById(pid);
}
if (result) { // 删除成功
if (cid == null) {
FileUtil.del(Constants.File.TESTCASE_BASE_FOLDER.getPath() + File.separator + "problem_" + pid);
}
return CommonResult.successResponse(null, "删除成功!");
} else {
return CommonResult.errorResponse("删除失败!", CommonResult.STATUS_FAIL);
if (cid == null) {
FileUtil.del(Constants.File.TESTCASE_BASE_FOLDER.getPath() + File.separator + "problem_" + pid);
}
return CommonResult.successResponse(null, "删除成功!");
}
@PostMapping("/problem")

View File

@ -14,9 +14,11 @@ import org.springframework.web.bind.annotation.*;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.crawler.problem.ProblemStrategy;
import top.hcode.hoj.pojo.dto.TrainingDto;
import top.hcode.hoj.pojo.entity.contest.Contest;
import top.hcode.hoj.pojo.entity.problem.Problem;
import top.hcode.hoj.pojo.entity.training.Training;
import top.hcode.hoj.pojo.entity.training.TrainingProblem;
import top.hcode.hoj.pojo.vo.TrainingVo;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.problem.impl.ProblemServiceImpl;
import top.hcode.hoj.service.training.impl.TrainingProblemServiceImpl;
@ -64,10 +66,11 @@ public class AdminTrainingController {
keyword = keyword.trim();
queryWrapper
.like("title", keyword).or()
.like("id", keyword);
.like("id", keyword).or()
.like("`rank`", keyword);
}
queryWrapper.orderByAsc("rank");
queryWrapper.orderByAsc("`rank`");
IPage<Training> TrainingPager = trainingService.page(iPage, queryWrapper);
if (TrainingPager.getTotal() == 0) { // 未查询到一条数据
@ -81,30 +84,14 @@ public class AdminTrainingController {
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
public CommonResult getTraining(@RequestParam("tid") Long tid, HttpServletRequest request) {
// 获取本场训练的信息
Training training = trainingService.getById(tid);
if (training == null) { // 查询不存在
return CommonResult.errorResponse("查询失败:该训练不存在,请检查参数tid是否准确");
}
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 是否为超级管理员
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
// 只有超级管理员和训练拥有者才能操作
if (!isRoot && !userRolesVo.getUsername().equals(training.getAuthor())) {
return CommonResult.errorResponse("对不起,你无权限操作!", CommonResult.STATUS_FORBIDDEN);
}
return CommonResult.successResponse(training, "查询成功!");
return trainingService.getAdminTrainingDto(tid, request);
}
@DeleteMapping("")
@RequiresAuthentication
@RequiresRoles(value = "root") // 只有超级管理员能删除训练
public CommonResult deleteTraining(@RequestParam("cid") Long cid) {
boolean result = trainingService.removeById(cid);
public CommonResult deleteTraining(@RequestParam("tid") Long tid) {
boolean result = trainingService.removeById(tid);
/*
Training的id为其他表的外键的表中的对应数据都会被一起删除
*/
@ -151,6 +138,33 @@ public class AdminTrainingController {
}
}
@PutMapping("/change-training-status")
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
public CommonResult changeTrainingStatus(@RequestParam(value = "tid", required = true) Long tid,
@RequestParam(value = "author", required = true) String author,
@RequestParam(value = "status", required = true) Boolean status,
HttpServletRequest request) {
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 是否为超级管理员
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
// 只有超级管理员和比赛拥有者才能操作
if (!isRoot && !userRolesVo.getUsername().equals(author)) {
return CommonResult.errorResponse("对不起,你无权限操作!", CommonResult.STATUS_FORBIDDEN);
}
boolean result = trainingService.saveOrUpdate(new Training().setId(tid).setStatus(status));
if (result) { // 添加成功
return CommonResult.successResponse(null, "修改成功!");
} else {
return CommonResult.errorResponse("修改失败", CommonResult.STATUS_FAIL);
}
}
@GetMapping("/get-problem-list")
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
@ -167,15 +181,16 @@ public class AdminTrainingController {
return CommonResult.successResponse(trainingProblemMap, "获取成功");
}
@GetMapping("/problem")
@PutMapping("/problem")
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
public CommonResult getProblem(@Valid @RequestParam("pid") Long pid) {
Problem problem = problemService.getById(pid);
if (problem != null) { // 查询成功
return CommonResult.successResponse(problem, "查询成功!");
@RequiresRoles(value = {"root", "problem_admin"}, logical = Logical.OR)
public CommonResult updateProblem(@RequestBody TrainingProblem trainingProblem) {
boolean isOk = trainingProblemService.saveOrUpdate(trainingProblem);
if (isOk) { // 删除成功
return CommonResult.successResponse(null, "修改成功!");
} else {
return CommonResult.errorResponse("查询失败!", CommonResult.STATUS_FAIL);
return CommonResult.errorResponse("修改失败!", CommonResult.STATUS_FAIL);
}
}
@ -249,9 +264,9 @@ public class AdminTrainingController {
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
@Transactional(rollbackFor = Exception.class)
public CommonResult importTrainingRemoteOJProblem(@RequestParam("name") String name,
@RequestParam("problemId") String problemId,
@RequestParam("tid") Long tid,
HttpServletRequest request) {
@RequestParam("problemId") String problemId,
@RequestParam("tid") Long tid,
HttpServletRequest request) {
QueryWrapper<Problem> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("problem_id", name.toUpperCase() + "-" + problemId);

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.dto.ReplyDto;
@ -57,7 +58,6 @@ public class CommentController {
private UserAcproblemServiceImpl userAcproblemService;
@GetMapping("/comments")
public CommonResult getComments(@RequestParam(value = "cid", required = false) Long cid,
@RequestParam(value = "did", required = false) Integer did,
@ -110,12 +110,17 @@ public class CommentController {
@RequiresAuthentication
@Transactional
public CommonResult addComment(@RequestBody Comment comment, HttpServletRequest request) {
if (StringUtils.isEmpty(comment.getContent().trim())) {
return CommonResult.errorResponse("评论内容不能为空!");
}
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 比赛外的评论 除管理员外 只有AC 10道以上才可评论
if (comment.getCid() == null ) {
if (comment.getCid() == null) {
if (!SecurityUtils.getSubject().hasRole("root")
&& !SecurityUtils.getSubject().hasRole("admin")
&& !SecurityUtils.getSubject().hasRole("problem_admin")) {
@ -287,6 +292,11 @@ public class CommentController {
@RequiresPermissions("reply_add")
@RequiresAuthentication
public CommonResult addReply(@RequestBody ReplyDto replyDto, HttpServletRequest request) {
if (StringUtils.isEmpty(replyDto.getReply().getContent().trim())) {
return CommonResult.errorResponse("回复内容不能为空!");
}
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
@ -323,9 +333,9 @@ public class CommentController {
reply.getToUid(),
reply.getFromUid());
}
return CommonResult.successResponse(reply, "评论成功");
return CommonResult.successResponse(reply, "回复成功");
} else {
return CommonResult.errorResponse("评论失败,请重新尝试!");
return CommonResult.errorResponse("回复失败,请重新尝试!");
}
}

View File

@ -174,7 +174,7 @@ public class ContestController {
QueryWrapper<ContestRegister> wrapper = new QueryWrapper<ContestRegister>().eq("cid", cid)
.eq("uid", userRolesVo.getUid());
if (contestRegisterService.getOne(wrapper,false) != null) {
if (contestRegisterService.getOne(wrapper, false) != null) {
return CommonResult.errorResponse("您已注册过该比赛,请勿重复注册!");
}
@ -205,9 +205,24 @@ public class ContestController {
QueryWrapper<ContestRegister> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("cid", cid).eq("uid", userRolesVo.getUid());
ContestRegister contestRegister = contestRegisterService.getOne(queryWrapper,false);
ContestRegister contestRegister = contestRegisterService.getOne(queryWrapper, false);
boolean access = false;
if (contestRegister != null) {
access = true;
Contest contest = contestService.getById(cid);
if (contest == null || !contest.getVisible()) {
return CommonResult.errorResponse("对不起,该比赛不存在!");
}
if (contest.getOpenAccountLimit()
&& !contestService.checkAccountRule(contest.getAccountLimitRule(), userRolesVo.getUsername())) {
access = false;
contestRecordService.removeById(contestRegister.getId());
}
}
HashMap<String, Object> result = new HashMap<>();
result.put("access", contestRegister != null);
result.put("access", access);
return CommonResult.successResponse(result);
}
@ -438,7 +453,6 @@ public class ContestController {
if (commonJudgeList.getTotal() == 0) { // 未查询到一条数据
return CommonResult.successResponse(commonJudgeList, "暂无数据");
} else {
// 比赛还是进行阶段同时不是超级管理员与比赛管理员需要将除自己之外的提交的时间空间长度隐藏
if (contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode()
&& !isRoot && !userRolesVo.getUid().equals(contest.getUid())) {

View File

@ -301,11 +301,12 @@ public class JudgeController {
// 不是本人的话不能查看代码时间空间长度
if (!userRolesVo.getUid().equals(judge.getUid())) {
judge.setCode(null);
// 如果还在比赛时间不是本人不能查看时间空间长度
// 如果还在比赛时间不是本人不能查看时间空间长度错误提示信息
if (contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode()) {
judge.setTime(null);
judge.setMemory(null);
judge.setLength(null);
judge.setErrorMessage("The contest is in progress. You are not allowed to view other people's error information.");
}
}
}
@ -438,6 +439,7 @@ public class JudgeController {
*/
@RequestMapping(value = "/check-submissions-status", method = RequestMethod.POST)
public CommonResult checkCommonJudgeResult(@RequestBody SubmitIdListDto submitIdListDto) {
List<Long> submitIds = submitIdListDto.getSubmitIds();
if (submitIds.size() == 0) {
@ -492,6 +494,11 @@ public class JudgeController {
List<Judge> judgeList = judgeService.list(queryWrapper);
HashMap<Long, Object> result = new HashMap<>();
for (Judge judge : judgeList) {
if (!judge.getUid().equals(userRolesVo.getUid())){
judge.setTime(null);
judge.setMemory(null);
judge.setLength(null);
}
result.put(judge.getSubmitId(), judge);
}
return CommonResult.successResponse(result, "获取最新判题数据成功!");

View File

@ -9,6 +9,8 @@ import org.springframework.stereotype.Repository;
import top.hcode.hoj.pojo.entity.training.Training;
import top.hcode.hoj.pojo.vo.TrainingVo;
import java.util.List;
/**
* @Author: Himit_ZH
* @Date: 2021/11/19 22:03
@ -18,8 +20,7 @@ import top.hcode.hoj.pojo.vo.TrainingVo;
@Repository
public interface TrainingMapper extends BaseMapper<Training> {
IPage<TrainingVo> getTrainingList(Page<TrainingVo> page,
@Param("categoryId") Long categoryId,
@Param("auth") String auth,
@Param("keyword") String keyword);
List<TrainingVo> getTrainingList(@Param("categoryId") Long categoryId,
@Param("auth") String auth,
@Param("keyword") String keyword);
}

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.hcode.hoj.dao.AuthMapper">
</mapper>

View File

@ -9,33 +9,35 @@
<result column="author" property="author"></result>
<result column="auth" property="auth"></result>
<result column="rank" property="rank"></result>
<result column="problem_count" property="problemCount"></result>
<result column="category_name" property="categoryName"></result>
<result column="category_color" property="categoryColor"></result>
<result column="problem_count" property="problemCount"></result>
<result column="gmt_modified" property="gmtModified"></result>
</resultMap>
<select id="getTrainingList" resultMap="map_TrainingList">
select t.*,tc.name as category_name,tc.color as category_color
SELECT * FROM (
select t.*,tc.name as category_name,tc.color as category_color
from training t,mapping_training_category mtc,training_category tc
<where>
t.status = true and t.id = mtc.tid and mtc.cid = tc.id
<if test="categoryId != null">
and tc.id = #{categoryId}
</if>
<if test="auth != null">
and t.auth = #{auth}
</if>
<if test="keyword != null and keyword != ''">
and (
t.title like concat('%',#{keyword},'%') or t.author like concat('%',#{keyword},'%')
)
</if>
</where>
) t LEFT JOIN
(
select count(*) from training_problem tp,problem p
where tp.tid = t.id and tp.pid = p.id and p.auth = 1
)
as problem_count
from training t,mapping_training_category mtc,training_category tc
<where>
t.status = true and t.id = mtc.tid and mtc.cid = tc.id
<if test="categoryId != null">
and tc.id = #{categoryId}
</if>
<if test="auth != null">
and t.auth = #{auth}
</if>
<if test="keyword != null and keyword != ''">
and (
t.title like concat('%',#{keyword},'%') or t.author like concat('%',#{keyword},'%')
)
</if>
</where>
order by t.rank asc
SELECT tp.tid,COUNT(*) AS problem_count FROM training_problem tp,problem p
WHERE tp.pid = p.id AND p.auth = 1 GROUP BY tp.tid
) tp
ON t.id = tp.tid ORDER BY t.`rank` ASC;
</select>
</mapper>

View File

@ -39,7 +39,7 @@
and j.pid = p.id
and p.auth = 1
and tp.tid = #{tid}
order by tp.rank asc
order by tp.`rank` asc
</select>
<!-- 子查询 :为了防止分页总数据数出错-->

View File

@ -5,6 +5,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* @Author: Himit_ZH
@ -30,9 +31,6 @@ public class TrainingVo implements Serializable {
@ApiModelProperty(value = "训练题单权限类型Public、Private")
private String auth;
@ApiModelProperty(value = "训练题单题目类型ACM、OI")
private String type;
@ApiModelProperty(value = "训练题单的分类名称")
private String categoryName;
@ -44,4 +42,7 @@ public class TrainingVo implements Serializable {
@ApiModelProperty(value = "该训练的总题数")
private Integer problemCount;
@ApiModelProperty(value = "训练更新时间")
private Date gmtModified;
}

View File

@ -97,8 +97,9 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, Contest> impl
return false;
} else if (contest.getSealRank() && contest.getSealRankTime() != null) { // 该比赛开启封榜模式
Date now = new Date();
// 如果现在时间处于封榜开始到比赛结束之间不可刷新榜单
if (now.after(contest.getSealRankTime()) && now.before(contest.getEndTime())) {
// 如果现在时间处于封榜开始到比赛结束之间或者没有开启自动解除封榜不可刷新榜单
if ((now.after(contest.getSealRankTime()) && now.before(contest.getEndTime()))
|| !contest.getAutoRealRank()) {
return true;
}
}
@ -147,7 +148,7 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, Contest> impl
if (!StringUtils.isEmpty(extra)) {
String[] accountList = extra.trim().split(" ");
for (String account : accountList) {
if (username.equals(account)){
if (username.equals(account)) {
return true;
}
}

View File

@ -2,10 +2,13 @@ package top.hcode.hoj.service.training;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.dto.TrainingDto;
import top.hcode.hoj.pojo.entity.training.Training;
import top.hcode.hoj.pojo.vo.TrainingVo;
import javax.servlet.http.HttpServletRequest;
public interface TrainingService extends IService<Training> {
public IPage<TrainingVo> getTrainingList(int limit, int currentPage,
Long categoryId, String auth, String keyword);
@ -13,4 +16,6 @@ public interface TrainingService extends IService<Training> {
public boolean addTraining(TrainingDto trainingDto);
public boolean updateTraining(TrainingDto trainingDto);
public CommonResult getAdminTrainingDto(Long tid,HttpServletRequest request);
}

View File

@ -15,10 +15,8 @@ import top.hcode.hoj.service.problem.impl.ProblemServiceImpl;
import top.hcode.hoj.service.training.TrainingProblemService;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Author: Himit_ZH
@ -54,14 +52,14 @@ public class TrainingProblemServiceImpl extends ServiceImpl<TrainingProblemMappe
trainingProblemQueryWrapper.eq("tid", tid).orderByAsc("display_id");
List<Long> pidList = new LinkedList<>();
List<TrainingProblem> trainingProblemList = trainingProblemMapper.selectList(trainingProblemQueryWrapper);
HashMap<Long, Object> trainingProblemMap = new HashMap<>();
HashMap<Long, TrainingProblem> trainingProblemMap = new HashMap<>();
trainingProblemList.forEach(trainingProblem -> {
trainingProblemMap.put(trainingProblem.getPid(), trainingProblem);
pidList.add(trainingProblem.getPid());
});
HashMap<String, Object> trainingProblem = new HashMap<>();
if (pidList.size() == 0) { // 该训练原本就无题目数据
if (pidList.size() == 0 && queryExisted) { // 该训练原本就无题目数据
trainingProblem.put("problemList", pidList);
trainingProblem.put("contestProblemMap", trainingProblemMap);
return trainingProblem;
@ -85,8 +83,16 @@ public class TrainingProblemServiceImpl extends ServiceImpl<TrainingProblemMappe
.like("author", keyword));
}
IPage<Problem> problemList = problemService.page(iPage, problemQueryWrapper);
trainingProblem.put("problemList", problemList);
IPage<Problem> problemListPager = problemService.page(iPage, problemQueryWrapper);
if (queryExisted) {
List<Problem> sortProblemList = problemListPager.getRecords()
.stream()
.sorted(Comparator.comparingInt(problem -> trainingProblemMap.get(problem.getId()).getRank()))
.collect(Collectors.toList());
problemListPager.setRecords(sortProblemList);
}
trainingProblem.put("problemList", problemListPager);
trainingProblem.put("trainingProblemMap", trainingProblemMap);
return trainingProblem;
}

View File

@ -5,8 +5,10 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.dao.MappingTrainingCategoryMapper;
import top.hcode.hoj.dao.TrainingMapper;
import top.hcode.hoj.pojo.dto.TrainingDto;
@ -14,9 +16,14 @@ import top.hcode.hoj.pojo.entity.training.MappingTrainingCategory;
import top.hcode.hoj.pojo.entity.training.Training;
import top.hcode.hoj.pojo.entity.training.TrainingCategory;
import top.hcode.hoj.pojo.vo.TrainingVo;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.training.TrainingService;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Himit_ZH
@ -37,8 +44,20 @@ public class TrainingServiceImpl extends ServiceImpl<TrainingMapper, Training> i
@Override
public IPage<TrainingVo> getTrainingList(int limit, int currentPage, Long categoryId, String auth, String keyword) {
List<TrainingVo> trainingList = trainingMapper.getTrainingList(categoryId, auth, keyword);
Page<TrainingVo> page = new Page<>(currentPage, limit);
return trainingMapper.getTrainingList(page, categoryId, auth, keyword);
int count = trainingList.size();
List<TrainingVo> pageList = new ArrayList<>();
//计算当前页第一条数据的下标
int currId = currentPage > 1 ? (currentPage - 1) * limit : 0;
for (int i = 0; i < limit && i < count - currId; i++) {
pageList.add(trainingList.get(currId + i));
}
page.setSize(limit);
page.setCurrent(currentPage);
page.setTotal(count);
page.setRecords(pageList);
return page;
}
@Override
@ -84,4 +103,34 @@ public class TrainingServiceImpl extends ServiceImpl<TrainingMapper, Training> i
int update = mappingTrainingCategoryMapper.update(null, updateWrapper);
return update > 0;
}
@Override
public CommonResult getAdminTrainingDto(Long tid, HttpServletRequest request) {
// 获取本场训练的信息
Training training = trainingMapper.selectById(tid);
if (training == null) { // 查询不存在
return CommonResult.errorResponse("查询失败:该训练不存在,请检查参数tid是否准确");
}
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 是否为超级管理员
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
// 只有超级管理员和训练拥有者才能操作
if (!isRoot && !userRolesVo.getUsername().equals(training.getAuthor())) {
return CommonResult.errorResponse("对不起,你无权限操作!", CommonResult.STATUS_FORBIDDEN);
}
TrainingDto trainingDto = new TrainingDto();
trainingDto.setTraining(training);
QueryWrapper<MappingTrainingCategory> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("tid", tid);
MappingTrainingCategory mappingTrainingCategory = mappingTrainingCategoryMapper.selectOne(queryWrapper);
TrainingCategory trainingCategory = trainingCategoryService.getById(mappingTrainingCategory.getCid());
trainingDto.setTrainingCategory(trainingCategory);
return CommonResult.successResponse(trainingDto, "查询成功!");
}
}

View File

@ -12,7 +12,9 @@ import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpException;
import org.apache.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;
import top.hcode.hoj.remoteJudge.task.RemoteJudgeStrategy;
import top.hcode.hoj.util.Constants;
@ -83,7 +85,21 @@ public class CodeForcesJudge implements RemoteJudgeStrategy {
problemNum = ReUtil.get("[0-9]+([A-Z]{1}[0-9]{0,1})", problemId, 1);
}
long nowTime = DateUtil.currentSeconds();
submitCode(contestId, problemNum, getLanguage(language), userCode);
try {
submitCode(contestId, problemNum, getLanguage(language), userCode);
} catch (HttpException e) {
// 如果提交出现403可能是cookie失效了再执行登录重新提交
Map<String, Object> loginUtils = getLoginUtils(username, password);
int status = (int) loginUtils.get("status");
if (status != HttpStatus.SC_MOVED_TEMPORARILY) {
log.error("进行题目提交时发生错误:登录失败,可能原因账号或密码错误,登录失败!" + CodeForcesJudge.class.getName() + ",题号:" + problemId);
return null;
}
submitCode(contestId, problemNum, getLanguage(language), userCode);
}
try {
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
@ -229,7 +245,7 @@ public class CodeForcesJudge implements RemoteJudgeStrategy {
return IMAGE_HOST + SUBMIT_URL;
}
public void submitCode(String contestId, String problemID, String languageID, String code) {
public void submitCode(String contestId, String problemID, String languageID, String code) throws HttpException {
String csrfToken = getCsrfToken(getSubmitUrl(contestId));
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("csrf_token", csrfToken);
@ -252,14 +268,14 @@ public class CodeForcesJudge implements RemoteJudgeStrategy {
if (response.getStatus() != HttpStatus.SC_MOVED_TEMPORARILY) {
if (response.body().contains("error for__programTypeId")) {
String log = String.format("Codeforces[%s] [%s]:Failed to submit code, caused by `Language Rejected`", contestId, problemID);
throw new RuntimeException(log);
throw new IllegalArgumentException(log);
}
if (response.body().contains("error for__source")) {
String log = String.format("Codeforces[%s] [%s]:Failed to submit code, caused by `Source Code Error`", contestId, problemID);
throw new RuntimeException(log);
throw new IllegalArgumentException(log);
}
String log = String.format("Codeforces[%s] [%s]:Failed to submit code, caused by `%s`", contestId, problemID,response.body());
throw new RuntimeException(log);
String log = String.format("Codeforces[%s] [%s]:Failed to submit code, caused by `403 Forbidden`", contestId, problemID);
throw new HttpException(log);
}
}

View File

@ -72,6 +72,9 @@ public class Contest implements Serializable {
@ApiModelProperty(value = "封榜起始时间,一直到比赛结束,不刷新榜单")
private Date sealRankTime;
@ApiModelProperty(value = "比赛结束是否自动解除封榜,自动转换成真实榜单")
private Boolean autoRealRank;
@ApiModelProperty(value = "-1为未开始0为进行中1为已结束")
private Integer status;

View File

@ -50,6 +50,7 @@ public class Training implements Serializable {
private Boolean status;
@ApiModelProperty(value = "编号,升序排序")
@TableField("`rank`")
private Integer rank;
@TableField(fill = FieldFill.INSERT)

View File

@ -40,6 +40,7 @@ public class TrainingProblem implements Serializable {
private String displayId;
@ApiModelProperty(value = "排序用")
@TableField("`rank`")
private Integer rank;
@TableField(fill = FieldFill.INSERT)

View File

@ -286,30 +286,35 @@ a:hover {
background-color: rgb(255, 153, 203);
}
.oi-100,
.first-ac {
.oi-100 {
background-color: #19be6b;
color: #fff;
font-weight: 700;
}
.oi-0 {
color: #a94442;
background-color: #f2dede;
}
.oi-between {
background-color: #2d8cf0;
color: #fff;
}
.first-ac {
background-color: #1daa1d;
}
.ac {
background-color: #a9f5af;
color: #3c763d;
background-color: #60e760;
}
.wa {
background-color: #e87272;
}
.try {
background-color: #ff9800;
color: #fff;
}
.oi-0,
.wa {
color: #a94442;
background-color: #f2dede;
}
.status-green {
background-color: #19be6b !important;
color: #fff !important;
@ -334,7 +339,7 @@ a:hover {
background: rgb(230, 255, 223) !important;
}
.vxe-table {
color: #495060 !important;
color: #000 !important;
font-size: 12px !important;
font-weight: 500 !important;
}

View File

@ -309,6 +309,84 @@ const ojApi = {
})
},
// ------------------------------------训练模块的请求---------------------------------------------
// 获取训练分类列表
getTrainingCategoryList(){
return ajax('/api/get-training-category', 'get')
},
// 获取训练列表
getTrainingList(currentPage,limit,query){
let params = {
currentPage,
limit
}
if(query!==undefined){
Object.keys(query).forEach((element) => {
if (query[element]) {
params[element] = query[element]
}
})
}
return ajax('/api/get-training-list','get',{
params: params
})
},
// 获取训练详情
getTraining(tid){
return ajax('/api/get-training-detail','get',{
params: {tid}
})
},
// 注册私有训练
registerTraining(tid, training){
return ajax('/api/register-contest','post',{
data:{
tid,
password
}
})
},
// 获取注册训练权限
getTrainingAccess(tid){
return ajax('/api/get-training-access','get',{
params: {tid}
})
},
// 获取训练题目列表
getTrainingProblemList(tid){
return ajax('/api/get-training-problem-list','get',{
params: {tid}
})
},
// 获取训练题目详情
getTrainingProblem(displayId,cid){
return ajax('/api/get-training-problem-details','get',{
params: {displayId,cid}
})
},
// 获取训练提交列表
getTrainingSubmissionList (limit, params) {
params.limit = limit
return ajax('/api/submissions', 'get', {
params
})
},
// 获取训练记录榜单
getTrainingRank(params){
return ajax('/api/get-training-rank', 'get', {
params
})
},
// ------------------------------------------------------------------------------------------------
// 比赛列表页的请求
getContestList(currentPage,limit,query){
let params = {
@ -921,6 +999,129 @@ const adminApi = {
})
},
admin_addTag (data) {
return ajax('/api/admin/tag', 'post', {
data
})
},
admin_updateTag (data) {
return ajax('/api/admin/tag', 'put', {
data
})
},
admin_deleteTag (tid) {
return ajax('/api/admin/tag', 'delete', {
params: {
tid
}
})
},
admin_getTrainingList (currentPage, limit, keyword) {
let params = {currentPage, limit}
if (keyword) {
params.keyword = keyword
}
return ajax('/api/admin/training/list', 'get', {
params: params
})
},
admin_changeTrainingStatus(tid,status,author){
return ajax('/api/admin/training/change-training-status', 'put', {
params: {
tid,
status,
author
}
})
},
admin_getTrainingProblemList(params) {
params = utils.filterEmptyValue(params)
return ajax('/api/admin/training/get-problem-list', 'get', {
params
})
},
admin_deleteTrainingProblem (pid,tid) {
return ajax('/api/admin/training/problem', 'delete', {
params: {
pid,
tid
}
})
},
admin_addTrainingProblemFromPublic (data) {
return ajax('/api/admin/training/add-problem-from-public', 'post', {
data
})
},
admin_addTrainingRemoteOJProblem(name,problemId,tid){
return ajax("/api/admin/training/import-remote-oj-problem","get",{
params: {
name,
problemId,
tid,
}
})
},
admin_updateTrainingProblem(data){
return ajax('/api/admin/training/problem', 'put', {
data
})
},
admin_createTraining (data) {
return ajax('/api/admin/training', 'post', {
data
})
},
admin_getTraining (tid) {
return ajax('/api/admin/training', 'get', {
params: {
tid
}
})
},
admin_editTraining (data) {
return ajax('/api/admin/training', 'put', {
data
})
},
admin_deleteTraining(tid){
return ajax('/api/admin/training', 'delete', {
params: {
tid
}
})
},
admin_addCategory(data) {
return ajax('/api/admin/training/category', 'post', {
data
})
},
admin_updateCategory (data) {
return ajax('/api/admin/training/category', 'put', {
data
})
},
admin_deleteCategory (cid) {
return ajax('/api/admin/training/category', 'delete', {
params: {
cid
}
})
},
admin_getContestProblemInfo(pid,cid) {
return ajax('/api/admin/contest/contest-problem', 'get', {
params: {
@ -972,32 +1173,12 @@ const adminApi = {
data
})
},
admin_addProblemFromPublic (data) {
admin_addContestProblemFromPublic (data) {
return ajax('/api/admin/contest/add-problem-from-public', 'post', {
data
})
},
admin_addTag (data) {
return ajax('/api/admin/tag', 'post', {
data
})
},
admin_updateTag (data) {
return ajax('/api/admin/tag', 'put', {
data
})
},
admin_deleteTag (tid) {
return ajax('/api/admin/tag', 'delete', {
params: {
tid
}
})
},
exportProblems (data) {
return ajax('export_problem', 'post', {
data

View File

@ -177,6 +177,17 @@ export const CONTEST_STATUS_REVERSE = {
}
}
export const TRAINING_TYPE = {
'Public':{
color:'success',
name:'Public'
},
'Private':{
color:'danger',
name:'Private'
}
}
export const RULE_TYPE = {
ACM: 0,
OI: 1
@ -224,7 +235,12 @@ export const STORAGE_KEY = {
AUTHED: 'authed',
PROBLEM_CODE: 'hojProblemCode',
languages: 'languages',
CONTEST_ANNOUNCE:'hojContestAnnounce'
CONTEST_ANNOUNCE:'hojContestAnnounce',
individualLanguageAndTheme:'hojIndividualLanguageAndTheme'
}
export function buildIndividualLanguageAndThemeKey () {
return `${STORAGE_KEY.individualLanguageAndTheme}`
}
export function buildProblemCodeKey (problemID, contestID = null) {

View File

@ -26,7 +26,7 @@
<el-button
icon="el-icon-plus"
size="mini"
@click.native="handleAddProblem(row.id)"
@click.native="handleAddProblem(row.id, row.problemId)"
type="primary"
>
</el-button>
@ -51,7 +51,7 @@ import api from '@/common/api';
import myMessage from '@/common/message';
export default {
name: 'add-problem-from-public',
props: ['contestID'],
props: ['contestID', 'trainingID'],
data() {
return {
page: 1,
@ -64,13 +64,17 @@ export default {
};
},
mounted() {
api
.admin_getContest(this.contestID)
.then((res) => {
this.contest = res.data.data;
this.getPublicProblem();
})
.catch(() => {});
if (this.contestID) {
api
.admin_getContest(this.contestID)
.then((res) => {
this.contest = res.data.data;
this.getPublicProblem(1);
})
.catch(() => {});
} else if (this.trainingID) {
this.getPublicProblem(1);
}
},
methods: {
getPublicProblem(page) {
@ -81,9 +85,17 @@ export default {
limit: this.limit,
problemType: this.contest.type,
cid: this.contest.id,
tid: this.trainingID,
};
api
.admin_getContestProblemList(params)
let func = null;
if (this.contestID) {
func = 'admin_getContestProblemList';
} else if (this.trainingID) {
func = 'admin_getTrainingProblemList';
}
api[func](params)
.then((res) => {
this.loading = false;
this.total = res.data.data.problemList.total;
@ -93,28 +105,44 @@ export default {
this.loading = false;
});
},
handleAddProblem(problemID) {
this.$prompt(
this.$i18n.t('m.Enter_The_Problem_Display_ID_in_the_Contest'),
'Tips'
).then(
({ value }) => {
let data = {
pid: problemID,
cid: this.contestID,
displayId: value,
};
api.admin_addProblemFromPublic(data).then(
(res) => {
this.$emit('on-change');
myMessage.success(this.$i18n.t('m.Add_Successfully'));
this.getPublicProblem(this.page);
},
() => {}
);
},
() => {}
);
handleAddProblem(id, problemId) {
if (this.contestID) {
this.$prompt(
this.$i18n.t('m.Enter_The_Problem_Display_ID_in_the_Contest'),
'Tips'
).then(
({ value }) => {
let data = {
pid: id,
cid: this.contestID,
displayId: value,
};
api.admin_addContestProblemFromPublic(data).then(
(res) => {
this.$emit('on-change');
myMessage.success(this.$i18n.t('m.Add_Successfully'));
this.getPublicProblem(this.page);
},
() => {}
);
},
() => {}
);
} else {
let data = {
pid: id,
tid: this.trainingID,
displayId: problemId,
};
api.admin_addTrainingProblemFromPublic(data).then(
(res) => {
this.$emit('on-change');
myMessage.success(this.$i18n.t('m.Add_Successfully'));
this.getPublicProblem(this.page);
},
() => {}
);
}
},
filterByKeyword() {
this.getPublicProblem(this.page);

View File

@ -22,6 +22,10 @@
><i class="el-icon-s-grid"></i
>{{ $t('m.NavBar_Problem') }}</el-menu-item
>
<el-menu-item index="/training"
><i class="el-icon-s-claim"></i
>{{ $t('m.NavBar_Training') }}</el-menu-item
>
<el-menu-item index="/contest"
><i class="el-icon-trophy"></i
>{{ $t('m.NavBar_Contest') }}</el-menu-item
@ -364,6 +368,20 @@
}}</mu-list-item-title>
</mu-list-item>
<mu-list-item
button
to="/training"
@click="opendrawer = !opendrawer"
active-class="mobile-menu-active"
>
<mu-list-item-action>
<mu-icon value=":el-icon-s-claim" size="24"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>{{
$t('m.NavBar_Training')
}}</mu-list-item-title>
</mu-list-item>
<mu-list-item
button
to="/contest"

View File

@ -9,7 +9,7 @@
@current-change="onChange"
@size-change="onPageSizeChange"
:layout="layout"
:page-sizes="[10, 15, 30, 50, 100]"
:page-sizes="pageSizes"
:current-page="current"
:hide-on-single-page="total == 0"
></el-pagination>
@ -28,6 +28,11 @@ export default {
required: false,
type: Number,
},
pageSizes: {
required: false,
type: Array,
default: [10, 15, 30, 50, 100],
},
showSizer: {
required: false,
type: Boolean,

View File

@ -10,18 +10,22 @@ export const m = {
// /views/admin/Home.vue
Dashboard: 'Dashboard',
General: 'General',
User_Admin: 'User Admin',
User_Admin: 'Admin User',
Announcement_Admin: 'Announcement',
System_Config: 'System Config',
Problem_Admin: 'Problem',
Problem_List: 'Problem List',
Create_Problem: 'Create Problem',
Export_Import_Problem: 'Export | Import Problem',
Training_Admin: 'Training',
Training_List: 'Training List',
Create_Training: 'Create Training',
Admin_Category:'Admin Category',
Contest_Admin: 'Contest',
Contest_List: 'Contest List',
Create_Contest: 'Create Contest',
Discussion:'Discussion',
Discussion_Admin:'Discussion Admin',
Discussion_Admin:'Admin Discussion',
Home_Page:'Home Page',
Logout:'Logout',
@ -162,7 +166,7 @@ export const m = {
Add:'Add',
Remove:'Remove',
Delete_Problem_Tips:'Are you sure you want to delete this problem? Note: the relevant submission data for this issue will also be deleted.',
Remove_Problem_Tips:'Are you sure you want to remove the problem from the competition?',
Remove_Contest_Problem_Tips:'Are you sure you want to remove the problem from the contest?',
Add_Successfully:'Add Successfully',
Download_Testcase_Success:'The testcase of this problem has been downloaded successfully!',
Enter_The_Problem_Display_ID_in_the_Contest:'Enter The Problem Display ID in the Contest',
@ -234,11 +238,47 @@ export const m = {
Update_Tag:'Update Tag',
To_Add:'Add',
To_Update:'Update',
Create_Training:'Create Training',
Tag_Name:'Tag Name',
Tag_Color:'Tag Color',
Tag_Attribution:'Tag Attribution',
Delete_Tag_Tips:'Are you sure you want to delete this tag?',
// /views/admin/training/TrainingList.vue
Order_Number:'Order Number',
View_Training_Problem_List:'View Training Problem List',
Delete_Training_Tips:'This operation will delete the training and its submission, rank record and other data. Do you want to continue?',
// /views/admin/training/Training.vue
Training_rank:'Training Sort Number (Ascending Sort)',
Training_Title: 'Training Title',
Training_Description: 'Training Description',
Training_Auth: 'Training Auth',
Training_Category:'Training Category',
Public_Training: 'Public Training',
Private_Training: 'Private Training',
Training_Password:'Training Password',
Edit_Training:'Edit Training',
Create_Training:'Create Training',
Redirect_To_Category:'The category list of current training is empty. Please go to create category first!',
Redirect:'Redirect',
// /views/admin/training/TrainingProblemList.vue
Training_Problem_List:'Training Problem List',
Add_Training_Problem:'Add Training Problem',
Remove_Training_Problem_Tips:'Are you sure you want to remove the problem from the training?',
Training_Problem_Rank:'Title Display Order(Ascending)',
// /views/admin/training/Category.vue
Add_Category:'Add Category',
Update_Category:'Update Category',
To_Add:'Add',
To_Update:'Update',
Category_Name:'Category Name',
Category_Color:'Category Color',
Delete_Category_Tips:'Are you sure you want to delete this category?',
// /views/admin/problem/ImportAndExport.vue
Export_Problem:'Export Problem',
Export:'Export',
@ -254,8 +294,8 @@ 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?',
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
Contest_Title: 'Contest Title',
Contest_Description: 'Contest Description',
@ -271,6 +311,9 @@ export const m = {
Contest_Seal_Half_Hour:'Half an hour',
Contest_Seal_An_Hour:'An hour',
Contest_Seal_All_Hour:'All hours',
Auto_Real_Rank:'Auto_Real_Rank',
Real_Rank_After_Contest:'Real Rank After Contest',
Seal_Rank_After_Contest:'Seal Rank After Contest',
Edit_Contest:'Edit Contest',
Create_Contest:'Create Contest',
Contest_Duration_Check:'The duration of the contest cannot be less than or equal to zero!',

View File

@ -17,6 +17,10 @@ export const m = {
Problem_List: '题目列表',
Create_Problem: '增加题目',
Export_Import_Problem: '导入|导出题目',
Training_Admin: '训练管理',
Training_List: '训练列表',
Create_Training: '创建训练',
Admin_Category:'分类管理',
Contest_Admin: '比赛管理',
Contest_List: '比赛列表',
Create_Contest: '创建比赛',
@ -162,7 +166,7 @@ export const m = {
Add:'添加',
Remove:'移除',
Delete_Problem_Tips:'确定要删除此题目吗?注意:该问题的相关数据也将被彻底删除,包括题目详情、题目的提交记录等!',
Remove_Problem_Tips:'你是否确定要将该题目移出比赛?',
Remove_Contest_Problem_Tips:'你是否确定要将该题目移出比赛?',
Add_Successfully:'添加成功',
Download_Testcase_Success:'该题目的评测数据已经被成功下载!',
Enter_The_Problem_Display_ID_in_the_Contest:'请输入该题目在比赛中展示ID',
@ -246,6 +250,41 @@ export const m = {
Import_FPS_Problem:'导入FPS格式的题目',
Export_Problem_NULL_Tips:'选择导出的题目不能为空',
// /views/admin/training/TrainingList.vue
Order_Number:'序号',
View_Training_Problem_List:'查看训练题目列表',
Delete_Training_Tips:'此操作将删除该训练提交记录、榜单等数据, 是否继续?',
// /views/admin/training/Training.vue
Training_rank:'训练排序编号(升序)',
Training_Title: '训练标题',
Training_Description: '训练描述',
Training_Auth: '训练权限',
Training_Category:'训练分类',
Public_Training: '公开训练',
Private_Training: '私有训练',
Training_Password:'训练密码',
Edit_Training:'编辑训练',
Create_Training:'创建训练',
Redirect_To_Category:'当前训练的分类列表为空,请先前往创建分类!',
Redirect:'重定向',
// /views/admin/training/TrainingProblemList.vue
Training_Problem_List:'训练题目列表',
Add_Training_Problem:'添加训练题目',
Remove_Training_Problem_Tips:'你是否确定要将该题目移出训练?',
Training_Problem_Rank:'题目显示顺序(升序)',
// /views/admin/training/Category.vue
Add_Category:'添加分类',
Update_Category:'修改分类',
To_Add:'添加',
To_Update:'更新',
Create_Training:'创建训练',
Category_Name:'分类名称',
Category_Color:'分类颜色',
Delete_Category_Tips:'你是否确定删除该分类?',
// /views/admin/contest/ContestList.vue
Visible:'是否可见',
Info:'信息',
@ -270,6 +309,9 @@ export const m = {
Contest_Seal_Half_Hour:'比赛结束前半小时',
Contest_Seal_An_Hour:'比赛结束前一小时',
Contest_Seal_All_Hour:'比赛全程',
Auto_Real_Rank:'自动取消封榜',
Real_Rank_After_Contest:'比赛完取消封榜',
Seal_Rank_After_Contest:'比赛完继续封榜',
Edit_Contest:'编辑比赛',
Create_Contest:'创建比赛',
Contest_Duration_Check:'比赛时长不能小于0',

View File

@ -2,6 +2,7 @@ export const m = {
// /components/oj/common/NavBar.vue 导航栏
NavBar_Home: 'Home',
NavBar_Problem: 'Problem',
NavBar_Training: 'Training',
NavBar_Contest: 'Contest',
NavBar_Status: 'Status',
NavBar_Rank: 'Rank',

View File

@ -2,6 +2,7 @@ export const m = {
// /components/oj/common/NavBar.vue 导航栏
NavBar_Home: '首页',
NavBar_Problem: '题目',
NavBar_Training: '训练',
NavBar_Contest: '比赛',
NavBar_Status: '评测',
NavBar_Rank: '排名',

View File

@ -13,6 +13,10 @@ const Tag= ()=>import('@/views/admin/problem/Tag')
const ProblemImportAndExport= ()=>import('@/views/admin/problem/ImportAndExport')
const Contest= ()=>import('@/views/admin/contest/Contest')
const ContestList= ()=>import('@/views/admin/contest/ContestList')
const Training= ()=>import('@/views/admin/training/Training')
const TrainingList= ()=>import('@/views/admin/training/TrainingList')
const TrainingProblemList= ()=>import('@/views/admin/training/TrainingProblemList')
const TrainingCategory= ()=>import('@/views/admin/training/Category')
const DiscussionList= ()=>import('@/views/admin/discussion/Discussion')
const adminRoutes= [
{
@ -90,7 +94,37 @@ const adminRoutes= [
path: 'problem/batch-operation',
name: 'admin-problem_batch_operation',
component: ProblemImportAndExport,
meta: { title:'Export Import_Problem'},
meta: { title:'Export Import Problem'},
},
{
path: 'training/create',
name: 'admin-training-contest',
component: Training,
meta: { title:'Create Training'},
},
{
path: 'training',
name: 'admin-training-list',
component: TrainingList,
meta: { title:'Training List'}
},
{
path: 'training/:trainingId/edit',
name: 'admin-edit-training',
component: Training,
meta: { title:'Edit Training'}
},
{
path: 'training/:trainingId/problems',
name: 'admin-training-problem-list',
component: TrainingProblemList,
meta: { title:'Training Problem List'}
},
{
path: 'training/category',
name: 'admin-training-category',
component: TrainingCategory,
meta: { title:'Admin Category'}
},
{
path: 'contest/create',
@ -120,7 +154,7 @@ const adminRoutes= [
path: 'contest/:contestId/problems',
name: 'admin-contest-problem-list',
component: ProblemList,
meta: { title:'Contest Problem_List'}
meta: { title:'Contest Problem List'}
},
{
path: 'contest/:contestId/problem/create',

View File

@ -26,6 +26,8 @@ import Developer from "@/views/oj/about/Developer.vue"
import Message from "@/views/oj/message/message.vue"
import UserMsg from "@/views/oj/message/UserMsg.vue"
import SysMsg from "@/views/oj/message/SysMsg.vue"
import TrainingList from "@/views/oj/training/TrainingList.vue"
import TrainingDetails from "@/views/oj/training/TrainingDetails.vue"
import NotFound from "@/views/404.vue"
const ojRoutes = [
@ -53,60 +55,25 @@ const ojRoutes = [
component: Problem,
meta: { title: 'Problem Details' }
},
{
path: '/training',
name: 'TrainingList',
component: TrainingList,
meta: { title: 'Training' }
},
{
name: 'TrainingDetails',
path: '/training/:trainingID/',
component:TrainingDetails,
meta: {title: 'Training Details'},
children: []
},
{
path: '/contest',
name: 'ContestList',
component: ContestList,
meta: { title: 'Contest' }
},
{
path: '/status',
name: 'SubmissionList',
component: SubmissionList,
meta: { title: 'Status' }
},
{
path: '/submission-detail/:submitID',
name: 'SubmissionDeatil',
component: SubmissionDetails,
meta: {title: 'Submission Deatil' }
},
{
path: '/acm-rank',
name: 'ACM Rank',
component: ACMRank,
meta: { title: 'ACM Rank' }
},
{
path: '/oi-rank',
name: 'OI Rank',
component: OIRank,
meta: { title: 'OI Rank' }
},
{
path: '/reset-password',
name: 'SetNewPassword',
component: SetNewPassword,
meta: { title: 'Reset Password' }
},
{
name: 'UserHome',
path: '/user-home',
component: UserHome,
meta: { title: 'User Home' }
},
{
name: 'Setting',
path: '/setting',
component: Setting,
meta: { requireAuth: true, title: 'Setting' }
},
{
name: 'Logout',
path: '/logout',
component: Logout,
meta: { requireAuth: true, title: 'Logout' }
},
{
name: 'ContestDetails',
path: '/contest/:contestID/',
@ -181,6 +148,54 @@ const ojRoutes = [
}
]
},
{
path: '/status',
name: 'SubmissionList',
component: SubmissionList,
meta: { title: 'Status' }
},
{
path: '/submission-detail/:submitID',
name: 'SubmissionDeatil',
component: SubmissionDetails,
meta: {title: 'Submission Deatil' }
},
{
path: '/acm-rank',
name: 'ACM Rank',
component: ACMRank,
meta: { title: 'ACM Rank' }
},
{
path: '/oi-rank',
name: 'OI Rank',
component: OIRank,
meta: { title: 'OI Rank' }
},
{
path: '/reset-password',
name: 'SetNewPassword',
component: SetNewPassword,
meta: { title: 'Reset Password' }
},
{
name: 'UserHome',
path: '/user-home',
component: UserHome,
meta: { title: 'User Home' }
},
{
name: 'Setting',
path: '/setting',
component: Setting,
meta: { requireAuth: true, title: 'Setting' }
},
{
name: 'Logout',
path: '/logout',
component: Logout,
meta: { requireAuth: true, title: 'Logout' }
},
{
path: '/discussion',
name: 'AllDiscussion',

View File

@ -2,6 +2,7 @@ import Vue from 'vue'
import Vuex from 'vuex'
import user from '@/store/user'
import contest from "@/store/contest"
import training from "@/store/training"
import api from '@/common/api'
import i18n from '@/i18n'
import storage from '@/common/storage'
@ -122,7 +123,8 @@ const rootActions = {
export default new Vuex.Store({
modules: {
user,
contest
contest,
training
},
state: rootState,
getters: rootGetters,

View File

@ -0,0 +1,108 @@
import api from '@/common/api'
import { TRAINING_TYPE } from '@/common/constants'
const state = {
intoAccess: false, // 比赛进入权限
training: {
auth: TRAINING_TYPE.Public.name,
rankShowName:'username'
},
trainingProblemList: [],
itemVisible: {
table: true,
chart: true,
},
}
const getters = {
isTrainingAdmin: (state, getters, _, rootGetters) => {
return rootGetters.isAuthenticated &&
(state.training.author === rootGetters.userInfo.username || rootGetters.isSuperAdmin)
},
trainingMenuDisabled: (state, getters) => {
// 训练创建者和超级管理员可以直接查看
if (getters.isTrainingAdmin) return false
if (state.training.auth === TRAINING_TYPE.Private.name) {
// 私有训练需要通过验证密码方可查看比赛
return !state.intoAccess
}
},
// 是否需要显示密码验证框
passwordFormVisible: (state, getters) => {
// 如果是公开训练,或已注册过,管理员都不用再显示
return state.training.auth !== TRAINING_TYPE.Public.name &&!state.intoAccess && !getters.isTrainingAdmin
}
}
const mutations = {
changeTraining (state, payload) {
state.training = payload.training
},
changeTrainingItemVisible(state, payload) {
state.itemVisible = {...state.itemVisible, ...payload}
},
changeTrainingProblemList(state, payload) {
state.trainingProblemList = payload.trainingProblemList;
},
trainingIntoAccess(state, payload) {
state.intoAccess = payload.intoAccess
},
clearTraining (state) {
state.training = {}
state.trainingProblemList = []
state.intoAccess = false
state.itemVisible = {
table: true,
chart: true,
realName: false
}
}
}
const actions = {
getTraining ({commit, rootState, dispatch}) {
return new Promise((resolve, reject) => {
api.getTraining(rootState.route.params.trainingID).then((res) => {
resolve(res)
let training = res.data.data
commit('changeTraining', {training: training})
if (training.auth == TRAINING_TYPE.Private.name) {
dispatch('getTrainingAccess',{auth:TRAINING_TYPE.Private.name})
}
}, err => {
reject(err)
})
})
},
getTrainingProblemList ({commit, rootState}) {
return new Promise((resolve, reject) => {
api.getTrainingProblemList(rootState.route.params.trainingID).then(res => {
resolve(res)
commit('changeTrainingProblemList', {trainingProblemList: res.data.data})
}, (err) => {
commit('changeTrainingProblemList', {trainingProblemList: []})
reject(err)
})
})
},
getTrainingAccess ({commit, rootState},trainingType) {
return new Promise((resolve, reject) => {
api.getTrainingAccess(rootState.route.params.trainingID).then(res => {
if(trainingType.auth == TRAINING_TYPE.Private.name){
commit('trainingIntoAccess', {intoAccess: res.data.data.access})
}
resolve(res)
}).catch()
})
}
}
export default {
state,
mutations,
getters,
actions
}

View File

@ -52,6 +52,27 @@
>{{ $t('m.Export_Import_Problem') }}</el-menu-item
>
</el-submenu>
<el-submenu index="training">
<template slot="title"
><i
class="el-icon-s-claim"
aria-hidden="true"
style="font-size: 20px;"
></i
>{{ $t('m.Training_Admin') }}</template
>
<el-menu-item index="/admin/training">{{
$t('m.Training_List')
}}</el-menu-item>
<el-menu-item index="/admin/training/create">{{
$t('m.Create_Training')
}}</el-menu-item>
<el-menu-item index="/admin/training/category">{{
$t('m.Admin_Category')
}}</el-menu-item>
</el-submenu>
<el-submenu index="contest">
<template slot="title"
><i class="fa fa-trophy fa-size" aria-hidden="true"></i
@ -84,7 +105,7 @@
$t('m.Home_Page')
}}</el-breadcrumb-item>
<el-breadcrumb-item v-for="item in routeList" :key="item.path">
{{ $t('m.' + item.meta.title.replace(' ', '_')) }}
{{ $t('m.' + item.meta.title.replaceAll(' ', '_')) }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
@ -303,6 +324,65 @@
</mu-list-item>
</mu-list-item>
<mu-list-item
button
:ripple="false"
nested
:open="openSideMenu === 'training'"
@toggle-nested="openSideMenu = arguments[0] ? 'training' : ''"
>
<mu-list-item-action>
<mu-icon value=":el-icon-s-claim fa-size"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>{{
$t('m.Training_Admin')
}}</mu-list-item-title>
<mu-list-item-action>
<mu-icon
class="toggle-icon"
size="24"
value="keyboard_arrow_down"
></mu-icon>
</mu-list-item-action>
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/training"
@click="opendrawer = !opendrawer"
active-class="mobile-menu-active"
>
<mu-list-item-title>{{
$t('m.Training_List')
}}</mu-list-item-title>
</mu-list-item>
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/training/create"
@click="opendrawer = !opendrawer"
active-class="mobile-menu-active"
>
<mu-list-item-title>{{
$t('m.Create_Training')
}}</mu-list-item-title>
</mu-list-item>
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/training/category"
@click="opendrawer = !opendrawer"
active-class="mobile-menu-active"
>
<mu-list-item-title>{{
$t('m.Admin_Category')
}}</mu-list-item-title>
</mu-list-item>
</mu-list-item>
<mu-list-item
button
:ripple="false"

View File

@ -115,6 +115,21 @@
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item
:label="$t('m.Auto_Real_Rank')"
required
v-if="contest.sealRank"
>
<el-switch
v-model="contest.autoRealRank"
:active-text="$t('m.Real_Rank_After_Contest')"
:inactive-text="$t('m.Seal_Rank_After_Contest')"
>
</el-switch>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item :label="$t('m.Print_Func')" required>
<el-switch
@ -277,6 +292,7 @@ export default {
pwd: '',
sealRank: true,
sealRankTime: '', //
autoRealRank: true,
auth: 0,
openPrint: false,
rankShowName: 'username',
@ -297,6 +313,9 @@ export default {
this.title = this.$i18n.t('m.Edit_Contest');
this.disableRuleType = true;
this.getContestByCid();
} else {
this.title = this.$i18n.t('m.Create_Contest');
this.disableRuleType = false;
}
},
watch: {
@ -343,7 +362,7 @@ export default {
this.seal_rank_time = 2;
break;
}
if (!this.contest.accountLimitRule) {
if (this.contest.accountLimitRule) {
this.formRule = this.changeStrToAccountRule(
this.contest.accountLimitRule
);

View File

@ -289,10 +289,10 @@
:visible.sync="addProblemDialogVisible"
@close-on-click-modal="false"
>
<ContestAddProblem
<AddPublicProblem
:contestID="contestId"
@on-change="getProblemList"
></ContestAddProblem>
></AddPublicProblem>
</el-dialog>
<el-dialog
@ -341,14 +341,14 @@
<script>
import api from '@/common/api';
import utils from '@/common/utils';
import ContestAddProblem from '@/components/admin/ContestAddProblem.vue';
import AddPublicProblem from '@/components/admin/AddPublicProblem.vue';
import myMessage from '@/common/message';
import { REMOTE_OJ } from '@/common/constants';
import { mapGetters } from 'vuex';
export default {
name: 'ProblemList',
components: {
ContestAddProblem,
AddPublicProblem,
},
data() {
return {
@ -498,7 +498,7 @@ export default {
);
},
removeProblem(pid) {
this.$confirm(this.$i18n.t('m.Remove_Problem_Tips'), 'Tips', {
this.$confirm(this.$i18n.t('m.Remove_Contest_Problem_Tips'), 'Tips', {
type: 'warning',
}).then(
() => {

View File

@ -0,0 +1,190 @@
<template>
<div>
<el-card>
<div slot="header">
<span class="panel-title home-title">{{ $t('m.Admin_Category') }}</span>
<div class="filter">
<span>
<el-button
type="primary"
size="small"
@click="openCategoryDialog('add', null)"
icon="el-icon-plus"
>{{ $t('m.Add_Category') }}
</el-button>
</span>
</div>
</div>
<el-tag
:key="index"
v-for="(category, index) in categoryList"
closable
:color="category.color ? category.color : '#409eff'"
effect="dark"
:disable-transitions="false"
@close="deleteCategory(category)"
@click="openCategoryDialog('update', category)"
class="category"
>
{{ category.name }}
</el-tag>
<el-button
class="button-new-category"
size="small"
@click="openCategoryDialog('add', null)"
>+ New Category</el-button
>
</el-card>
<el-dialog
:title="$t('m.' + upsertTitle)"
width="350px"
:visible.sync="addCategoryDialogVisible"
@close-on-click-modal="false"
>
<el-form>
<el-form-item :label="$t('m.Category_Name')" required>
<el-input v-model="category.name" size="small"></el-input>
</el-form-item>
<el-form-item :label="$t('m.Category_Color')" required>
<el-color-picker v-model="category.color"></el-color-picker>
</el-form-item>
<el-form-item style="text-align:center">
<el-button
type="primary"
@click="upsertCategory"
:loading="upsertCategoryLoading"
>{{ $t('m.' + upsertCategoryBtn) }}
</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import myMessage from '@/common/message';
import api from '@/common/api';
export default {
data() {
return {
getCategoryListLoading: false,
categoryList: [],
addCategoryDialogVisible: false,
upsertTitle: 'Add_Category',
upsertCategoryBtn: 'To_Add',
upsertCategoryLoading: false,
category: {
id: null,
name: null,
color: null,
},
};
},
mounted() {
this.getTrainingCategoryList();
},
methods: {
getTrainingCategoryList() {
this.getCategoryListLoading = true;
api.getTrainingCategoryList().then(
(res) => {
this.categoryList = res.data.data;
this.getCategoryListLoading = false;
},
(err) => {
this.getCategoryListLoading = false;
}
);
},
deleteCategory(category) {
this.$confirm(this.$i18n.t('m.Delete_Category_Tips'), 'Tips', {
type: 'warning',
}).then(
() => {
api
.admin_deleteCategory(category.id)
.then((res) => {
myMessage.success(this.$i18n.t('m.Delete_successfully'));
this.categoryList.splice(this.categoryList.indexOf(category), 1);
})
.catch(() => {});
},
() => {}
);
},
openCategoryDialog(action, category) {
if (action == 'add') {
this.upsertTitle = 'Add_Category';
this.upsertCategoryBtn = 'To_Add';
this.category = {
id: null,
name: null,
color: null,
};
} else {
this.upsertTitle = 'Update_Category';
this.upsertCategoryBtn = 'To_Update';
this.category = Object.assign({}, category);
}
this.addCategoryDialogVisible = true;
},
upsertCategory() {
if (this.category.id) {
this.upsertCategoryLoading = true;
api.admin_updateCategory(this.category).then(
(res) => {
this.upsertCategoryLoading = false;
myMessage.success(this.$i18n.t('m.Update_Successfully'));
this.categoryList.push(res.data.data);
this.addCategoryDialogVisible = false;
this.getTrainingCategoryList();
},
(err) => {
this.upsertCategoryLoading = false;
}
);
} else {
this.upsertCategoryLoading = true;
api.admin_addCategory(this.category).then(
(res) => {
this.upsertCategoryLoading = false;
myMessage.success(this.$i18n.t('m.Add_Successfully'));
this.categoryList.push(res.data.data);
this.addCategoryDialogVisible = false;
},
(err) => {
this.upsertCategoryLoading = false;
}
);
}
},
},
};
</script>
<style scoped>
.filter {
margin-top: 10px;
}
.filter span {
margin-right: 10px;
}
.el-tag {
margin-left: 10px;
margin-top: 10px;
}
.category {
cursor: pointer;
}
.button-new-category {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
margin-top: 10px;
}
</style>

View File

@ -0,0 +1,250 @@
<template>
<div class="view">
<el-card>
<div slot="header">
<span class="panel-title home-title">
{{ title }}
</span>
</div>
<el-form label-position="top">
<el-row :gutter="20">
<el-col :span="24">
<el-form-item :label="$t('m.Training_rank')" required>
<el-input-number
v-model="training.rank"
@change="handleChange"
:min="0"
:max="2147483647"
:label="$t('m.Training_rank')"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item :label="$t('m.Training_Title')" required>
<el-input
v-model="training.title"
:placeholder="$t('m.Training_Title')"
></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item :label="$t('m.Training_Description')" required>
<Editor :value.sync="training.description"></Editor>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item :label="$t('m.Category')" required>
<el-select v-model="trainingCategoryId">
<el-option
:label="category.name"
:value="category.id"
v-for="(category, index) in trainingCategoryList"
:key="index"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item :label="$t('m.Training_Auth')" required>
<el-select v-model="training.auth">
<el-option
:label="$t('m.Public_Training')"
value="Public"
></el-option>
<el-option
:label="$t('m.Private_Training')"
value="Private"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item
:label="$t('m.Training_Password')"
v-show="training.auth != 'Public'"
:required="training.auth != 'Public'"
>
<el-input
v-model="training.privatePwd"
:placeholder="$t('m.Training_Password')"
></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-button type="primary" @click.native="saveTraining">{{
$t('m.Save')
}}</el-button>
</el-card>
</div>
</template>
<script>
import api from '@/common/api';
import { mapGetters } from 'vuex';
import myMessage from '@/common/message';
const Editor = () => import('@/components/admin/Editor.vue');
export default {
name: 'CreateTraining',
components: {
Editor,
},
data() {
return {
title: 'Create Training',
training: {
rank: 1000,
title: '',
description: '',
privatePwd: '',
auth: 'Public',
},
trainingCategoryId: null,
trainingCategoryList: [],
};
},
mounted() {
this.init();
},
watch: {
$route() {
if (this.$route.name === 'admin-edit-training') {
this.title = this.$i18n.t('m.Edit_Training');
this.getTraining();
} else {
this.title = this.$i18n.t('m.Create_Training');
this.training = {
rank: 1000,
title: '',
description: '',
privatePwd: '',
auth: 'Public',
};
}
},
},
computed: {
...mapGetters(['userInfo']),
},
methods: {
init() {
api.getTrainingCategoryList().then((res) => {
let data = res.data.data;
if (!data || !data.length) {
this.$alert(
this.$i18n.t('m.Redirect_To_Category'),
this.$i18n.t('m.Redirect'),
{
confirmButtonText: this.$i18n.t('m.OK'),
showClose: false,
callback: (action) => {
this.$router.push({
path: '/admin/training/category',
});
},
}
);
} else {
this.trainingCategoryList = data;
if (this.$route.name === 'admin-edit-training') {
this.title = this.$i18n.t('m.Edit_Training');
this.getTraining();
} else {
this.title = this.$i18n.t('m.Create_Training');
}
}
});
},
getTraining() {
api
.admin_getTraining(this.$route.params.trainingId)
.then((res) => {
let data = res.data.data;
this.training = data.training || {};
this.trainingCategoryId = data.trainingCategory.id || null;
})
.catch(() => {});
},
saveTraining() {
if (!this.training.rank && this.training.rank != 0) {
myMessage.error(
this.$i18n.t('m.Training_rank') + ' ' + this.$i18n.t('m.is_required')
);
return;
}
if (!this.training.title) {
myMessage.error(
this.$i18n.t('m.Training_Title') + ' ' + this.$i18n.t('m.is_required')
);
return;
}
if (!this.training.description) {
myMessage.error(
this.$i18n.t('m.Training_Description') +
' ' +
this.$i18n.t('m.is_required')
);
return;
}
if (!this.trainingCategoryId) {
myMessage.error(
this.$i18n.t('m.Training_Category') +
' ' +
this.$i18n.t('m.is_required')
);
return;
}
if (this.training.auth != 'Public' && !this.training.pwd) {
myMessage.error(
this.$i18n.t('m.Training_Password') +
' ' +
this.$i18n.t('m.is_required')
);
return;
}
let funcName =
this.$route.name === 'admin-edit-training'
? 'admin_editTraining'
: 'admin_createTraining';
let data = Object.assign({}, this.training);
if (funcName === 'admin_createTraining') {
data['author'] = this.userInfo.username;
}
let trainingDto = {
training: data,
trainingCategory: {
id: this.trainingCategoryId,
},
};
api[funcName](trainingDto)
.then((res) => {
myMessage.success('success');
this.$router.push({
name: 'admin-training-list',
query: { refresh: 'true' },
});
})
.catch(() => {});
},
},
};
</script>
<style scoped>
.userPreview {
padding-left: 10px;
padding-top: 20px;
padding-bottom: 20px;
color: red;
font-size: 16px;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,235 @@
<template>
<div>
<el-card>
<div slot="header">
<span class="panel-title home-title">{{ $t('m.Training_List') }}</span>
<div class="filter-row">
<span>
<vxe-input
v-model="keyword"
:placeholder="$t('m.Enter_keyword')"
type="search"
size="medium"
@search-click="filterByKeyword"
@keyup.enter.native="filterByKeyword"
></vxe-input>
</span>
</div>
</div>
<vxe-table
:loading="loading"
ref="xTable"
:data="trainingList"
auto-resize
stripe
align="center"
>
<vxe-table-column field="id" width="80" title="ID"> </vxe-table-column>
<vxe-table-column field="rank" width="80" :title="$t('m.Order_Number')">
</vxe-table-column>
<vxe-table-column
field="title"
min-width="150"
:title="$t('m.Title')"
show-overflow
>
</vxe-table-column>
<vxe-table-column :title="$t('m.Auth')" width="100">
<template v-slot="{ row }">
<el-tag :type="TRAINING_TYPE[row.auth]['color']" effect="dark">
{{ row.auth }}
</el-tag>
</template>
</vxe-table-column>
<vxe-table-column :title="$t('m.Visible')" min-width="80">
<template v-slot="{ row }">
<el-switch
v-model="row.status"
:disabled="!isSuperAdmin && userInfo.username != row.author"
@change="changeTrainingStatus(row.id, row.status, row.author)"
>
</el-switch>
</template>
</vxe-table-column>
<vxe-table-column min-width="210" :title="$t('m.Info')">
<template v-slot="{ row }">
<p>Created Time: {{ row.gmtCreate | localtime }}</p>
<p>Update Time: {{ row.gmtModified | localtime }}</p>
<p>Creator: {{ row.author }}</p>
</template>
</vxe-table-column>
<vxe-table-column min-width="150" :title="$t('m.Option')">
<template v-slot="{ row }">
<template v-if="isSuperAdmin || userInfo.uid == row.uid">
<div style="margin-bottom:10px">
<el-tooltip
effect="dark"
:content="$t('m.Edit')"
placement="top"
>
<el-button
icon="el-icon-edit"
size="mini"
@click.native="goEdit(row.id)"
type="primary"
>
</el-button>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('m.View_Training_Problem_List')"
placement="top"
>
<el-button
icon="el-icon-tickets"
size="mini"
@click.native="goTrainingProblemList(row.id)"
type="success"
>
</el-button>
</el-tooltip>
</div>
</template>
<el-tooltip
effect="dark"
:content="$t('m.Delete')"
placement="top"
v-if="isSuperAdmin"
>
<el-button
icon="el-icon-delete"
size="mini"
@click.native="deleteTraining(row.id)"
type="danger"
>
</el-button>
</el-tooltip>
</template>
</vxe-table-column>
</vxe-table>
<div class="panel-options">
<el-pagination
class="page"
layout="prev, pager, next"
@current-change="currentChange"
:page-size="pageSize"
:total="total"
>
</el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import api from '@/common/api';
import utils from '@/common/utils';
import { TRAINING_TYPE } from '@/common/constants';
import { mapGetters } from 'vuex';
import myMessage from '@/common/message';
export default {
name: 'TrainingList',
data() {
return {
pageSize: 10,
total: 0,
trainingList: [],
keyword: '',
loading: false,
excludeAdmin: true,
currentPage: 1,
currentId: 1,
downloadDialogVisible: false,
TRAINING_TYPE: {},
};
},
mounted() {
this.getTrainingList(this.currentPage);
this.TRAINING_TYPE = Object.assign({}, TRAINING_TYPE);
},
watch: {
$route() {
let refresh = this.$route.query.refresh == 'true' ? true : false;
if (refresh) {
this.getTrainingList(1);
}
},
},
computed: {
...mapGetters(['isSuperAdmin', 'userInfo']),
},
methods: {
//
currentChange(page) {
this.currentPage = page;
this.getTrainingList(page);
},
getTrainingList(page) {
this.loading = true;
api.admin_getTrainingList(page, this.pageSize, this.keyword).then(
(res) => {
this.loading = false;
this.total = res.data.data.total;
this.trainingList = res.data.data.records;
},
(res) => {
this.loading = false;
}
);
},
goEdit(trainingId) {
this.$router.push({
name: 'admin-edit-training',
params: { trainingId },
});
},
goTrainingProblemList(trainingId) {
this.$router.push({
name: 'admin-training-problem-list',
params: { trainingId },
});
},
deleteTraining(trainingId) {
this.$confirm(this.$i18n.t('m.Delete_Training_Tips'), 'Tips', {
confirmButtonText: this.$i18n.t('m.OK'),
cancelButtonText: this.$i18n.t('m.Cancel'),
type: 'warning',
}).then(() => {
api.admin_deleteTraining(trainingId).then((res) => {
myMessage.success(this.$i18n.t('m.Delete_successfully'));
this.currentChange(1);
});
});
},
changeTrainingStatus(trainingId, status, author) {
api.admin_changeTrainingStatus(trainingId, status, author).then((res) => {
myMessage.success(this.$i18n.t('m.Update_Successfully'));
});
},
filterByKeyword() {
this.currentChange(1);
},
},
};
</script>
<style scoped>
.filter-row {
margin-top: 10px;
}
@media screen and (max-width: 768px) {
.filter-row span {
margin-right: 5px;
}
.filter-row span div {
width: 80% !important;
}
}
@media screen and (min-width: 768px) {
.filter-row span {
margin-right: 20px;
}
}
.el-tag--dark {
border-color: #fff;
}
</style>

View File

@ -0,0 +1,426 @@
<template>
<div>
<el-card>
<div slot="header">
<span class="panel-title home-title">{{
$t('m.Training_Problem_List')
}}</span>
<div class="filter-row">
<span>
<el-button
type="primary"
size="small"
icon="el-icon-plus"
@click="addProblemDialogVisible = true"
>{{ $t('m.Add_From_Public_Problem') }}
</el-button>
</span>
<span>
<el-button
type="success"
size="small"
@click="AddRemoteOJProblemDialogVisible = true"
icon="el-icon-plus"
>{{ $t('m.Add_Rmote_OJ_Problem') }}
</el-button>
</span>
<span>
<vxe-input
v-model="keyword"
:placeholder="$t('m.Enter_keyword')"
type="search"
size="medium"
@search-click="filterByKeyword"
@keyup.enter.native="filterByKeyword"
></vxe-input>
</span>
</div>
</div>
<vxe-table
stripe
auto-resize
:data="problemList"
ref="adminProblemList"
:loading="loading"
align="center"
>
<vxe-table-column min-width="64" field="id" title="ID">
</vxe-table-column>
<vxe-table-column
min-width="100"
field="problemId"
:title="$t('m.Display_ID')"
>
</vxe-table-column>
<vxe-table-column
field="title"
min-width="150"
:title="$t('m.Title')"
show-overflow
>
</vxe-table-column>
<vxe-table-column
field="author"
min-width="130"
:title="$t('m.Author')"
show-overflow
>
</vxe-table-column>
<vxe-table-column
min-width="120"
:title="$t('m.Training_Problem_Rank')"
>
<template v-slot="{ row }">
<el-input-number
v-model="trainingProblemMap[row.id].rank"
@change="handleChangeRank(trainingProblemMap[row.id])"
:min="0"
:max="2147483647"
></el-input-number>
</template>
</vxe-table-column>
<vxe-table-column min-width="100" :title="$t('m.Auth')">
<template v-slot="{ row }">
<el-select
v-model="row.auth"
@change="changeProblemAuth(row)"
size="small"
:disabled="true"
>
<el-option
:label="$t('m.Public_Problem')"
:value="1"
:disabled="!isSuperAdmin && !isProblemAdmin"
></el-option>
<el-option
:label="$t('m.Private_Problem')"
:value="2"
:disabled="true"
></el-option>
<el-option
:label="$t('m.Contest_Problem')"
:value="3"
:disabled="true"
></el-option>
</el-select>
</template>
</vxe-table-column>
<vxe-table-column title="Option" min-width="200">
<template v-slot="{ row }">
<el-tooltip effect="dark" :content="$t('m.Edit')" placement="top">
<el-button
icon="el-icon-edit-outline"
size="mini"
@click.native="goEdit(row.id)"
type="primary"
>
</el-button>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('m.Download_Testcase')"
placement="top"
v-if="isSuperAdmin || isProblemAdmin"
>
<el-button
icon="el-icon-download"
size="mini"
@click.native="downloadTestCase(row.id)"
type="success"
>
</el-button>
</el-tooltip>
<el-tooltip effect="dark" :content="$t('m.Remove')" placement="top">
<el-button
icon="el-icon-close"
size="mini"
@click.native="removeProblem(row.id)"
type="warning"
>
</el-button>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('m.Delete')"
placement="top"
v-if="isSuperAdmin || isProblemAdmin"
>
<el-button
icon="el-icon-delete-solid"
size="mini"
@click.native="deleteProblem(row.id)"
type="danger"
>
</el-button>
</el-tooltip>
</template>
</vxe-table-column>
</vxe-table>
<div class="panel-options">
<el-pagination
class="page"
layout="prev, pager, next, sizes"
@current-change="currentChange"
:page-size="pageSize"
:total="total"
@size-change="onPageSizeChange"
:page-sizes="[10, 30, 50, 100]"
>
</el-pagination>
</div>
</el-card>
<el-dialog
:title="$t('m.Add_Training_Problem')"
width="90%"
:visible.sync="addProblemDialogVisible"
@close-on-click-modal="false"
>
<AddPublicProblem
:trainingID="trainingId"
@on-change="getProblemList"
></AddPublicProblem>
</el-dialog>
<el-dialog
:title="$t('m.Add_Rmote_OJ_Problem')"
width="350px"
:visible.sync="AddRemoteOJProblemDialogVisible"
@close-on-click-modal="false"
>
<el-form>
<el-form-item :label="$t('m.Remote_OJ')">
<el-select v-model="otherOJName" size="small">
<el-option
:label="remoteOj.name"
:value="remoteOj.key"
v-for="(remoteOj, index) in REMOTE_OJ"
:key="index"
></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('m.Problem_ID')" required>
<el-input v-model="otherOJProblemId" size="small"></el-input>
</el-form-item>
<el-form-item style="text-align:center">
<el-button
type="primary"
icon="el-icon-plus"
@click="addRemoteOJProblem"
:loading="addRemoteOJproblemLoading"
>{{ $t('m.Add') }}
</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import api from '@/common/api';
import AddPublicProblem from '@/components/admin/AddPublicProblem.vue';
import myMessage from '@/common/message';
import { REMOTE_OJ } from '@/common/constants';
import { mapGetters } from 'vuex';
export default {
name: 'ProblemList',
components: {
AddPublicProblem,
},
data() {
return {
problemListAuth: 0,
oj: 'All',
pageSize: 10,
total: 0,
problemList: [],
trainingProblemMap: {},
keyword: '',
loading: false,
currentPage: 1,
routeName: '',
trainingId: '',
// for make public use
currentProblemID: '',
currentRow: {},
addProblemDialogVisible: false,
AddRemoteOJProblemDialogVisible: false,
addRemoteOJproblemLoading: false,
otherOJName: 'HDU',
otherOJProblemId: '',
REMOTE_OJ: {},
displayId: '',
};
},
mounted() {
this.init();
},
computed: {
...mapGetters(['userInfo', 'isSuperAdmin', 'isProblemAdmin']),
},
methods: {
init() {
this.routeName = this.$route.name;
this.trainingId = this.$route.params.trainingId;
this.getProblemList(this.currentPage);
this.REMOTE_OJ = Object.assign({}, REMOTE_OJ);
},
goEdit(problemId) {
this.$router.push({
name: 'admin-edit-problem',
params: { problemId: problemId },
});
},
//
currentChange(page) {
this.currentPage = page;
this.getProblemList(page);
},
onPageSizeChange(pageSize) {
this.pageSize = pageSize;
this.getProblemList(this.currentPage);
},
getProblemList(page = 1) {
this.loading = true;
let params = {
limit: this.pageSize,
currentPage: page,
keyword: this.keyword,
tid: this.trainingId,
queryExisted: true,
};
if (this.problemListAuth != 0) {
params['auth'] = this.problemListAuth;
}
api.admin_getTrainingProblemList(params).then(
(res) => {
this.loading = false;
this.total = res.data.data.problemList.total;
this.problemList = res.data.data.problemList.records;
this.trainingProblemMap = res.data.data.trainingProblemMap;
},
(err) => {
this.loading = false;
}
);
},
handleChangeRank(data) {
api.admin_updateTrainingProblem(data).then((res) => {
myMessage.success(this.$i18n.t('m.Update_Successfully'));
this.getProblemList(1);
});
},
changeProblemAuth(row) {
api.admin_changeProblemPublic(row).then((res) => {
myMessage.success(this.$i18n.t('m.Update_Successfully'));
});
},
deleteProblem(id) {
this.$confirm(this.$i18n.t('m.Delete_Problem_Tips'), 'Tips', {
type: 'warning',
}).then(
() => {
api
.admin_deleteTrainingProblem(id, null)
.then((res) => {
myMessage.success(this.$i18n.t('m.Delete_successfully'));
this.getProblemList(this.currentPage);
})
.catch(() => {});
},
() => {}
);
},
removeProblem(pid) {
this.$confirm(this.$i18n.t('m.Remove_Training_Problem_Tips'), 'Tips', {
type: 'warning',
}).then(
() => {
api
.admin_deleteTrainingProblem(pid, this.trainingId)
.then((res) => {
myMessage.success('success');
this.getProblemList(this.currentPage);
})
.catch(() => {});
},
() => {}
);
},
downloadTestCase(problemID) {
let url = '/api/file/download-testcase?pid=' + problemID;
utils.downloadFile(url).then(() => {
this.$alert(this.$i18n.t('m.Download_Testcase_Success'), 'Tips');
});
},
filterByKeyword() {
this.currentChange(1);
},
addRemoteOJProblem() {
if (!this.otherOJProblemId) {
myMessage.error(this.$i18n.t('m.Problem_ID_is_required'));
return;
}
this.addRemoteOJproblemLoading = true;
api
.admin_addTrainingRemoteOJProblem(
this.otherOJName,
this.otherOJProblemId,
this.trainingId
)
.then(
(res) => {
this.addRemoteOJproblemLoading = false;
this.AddRemoteOJProblemDialogVisible = false;
myMessage.success(this.$i18n.t('m.Add_Successfully'));
this.currentChange(1);
},
(err) => {
this.addRemoteOJproblemLoading = false;
}
);
},
},
watch: {
$route(newVal, oldVal) {
if (
newVal.params.trainingId != oldVal.params.trainingId ||
newVal.name != oldVal.name
) {
this.init();
}
},
},
};
</script>
<style scoped>
.filter-row span button {
margin-top: 5px;
margin-bottom: 5px;
}
.filter-row span div {
margin-top: 8px;
}
@media screen and (max-width: 768px) {
.filter-row span {
margin-right: 5px;
}
.filter-row span div {
width: 80%;
}
}
@media screen and (min-width: 768px) {
.filter-row span {
margin-right: 20px;
}
}
</style>

View File

@ -71,7 +71,11 @@
style="text-align:center"
>
<div slot="header">
<span class="panel-title">{{ $t('m.Password_Required') }}</span>
<span class="panel-title" style="color: #e6a23c;"
><i class="el-icon-warning">
{{ $t('m.Password_Required') }}</i
></span
>
</div>
<p class="password-form-tips">
{{ $t('m.To_Enter_Need_Password') }}
@ -82,6 +86,7 @@
type="password"
:placeholder="$t('m.Enter_the_contest_password')"
@keydown.enter.native="checkPassword"
style="width:70%"
/>
<el-button
type="primary"

View File

@ -132,33 +132,33 @@
v-if="isContestAdmin"
>
</vxe-table-column>
<vxe-table-column
field="rating"
:title="$t('m.AC') + ' / ' + $t('m.Total')"
min-width="80"
>
<vxe-table-column field="rating" :title="$t('m.AC')" min-width="60">
<template v-slot="{ row }">
<span
>{{ row.ac }} /
<span>
<a
@click="getUserTotalSubmit(row.username)"
style="color:rgb(87, 163, 243);"
>{{ row.total }}</a
>
@click="getUserACSubmit(row.username)"
style="color:rgb(87, 163, 243);font-weight: 600;font-size: 14px;"
>{{ row.ac }}
</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="totalTime"
:title="$t('m.TotalTime')"
min-width="100"
min-width="60"
>
<template v-slot="{ row }">
<span>{{ parseTotalTime(row.totalTime) }}</span>
<el-tooltip effect="dark" placement="top">
<div slot="content">
{{ parseTimeToSpecific(row.totalTime) }}
</div>
<span>{{ parseInt(row.totalTime / 60) }}</span>
</el-tooltip>
</template>
</vxe-table-column>
<vxe-table-column
min-width="120"
min-width="74"
v-for="problem in contestProblems"
:key="problem.displayId"
>
@ -181,45 +181,63 @@
></path>
</svg>
</span>
<span
><a
@click="getContestProblemById(problem.displayId)"
class="emphasis"
style="color:#495060;"
>{{ problem.displayId }}</a
></span
>
<span>
<el-tooltip effect="dark" placement="top">
<div slot="content">
{{ problem.displayId + '. ' + problem.displayTitle }}
<br />
{{ 'Accepted: ' + problem.ac }}
<br />
{{ 'Rejected: ' + (problem.total - problem.ac) }}
</div>
<a
@click="getContestProblemById(problem.displayId)"
class="emphasis"
style="color:#495060;"
>{{ problem.displayId }}
</a>
</el-tooltip>
</span>
</template>
<template v-slot="{ row }">
<span v-if="row.submissionInfo[problem.displayId]">
<span v-if="row.submissionInfo[problem.displayId].isAC"
>{{ row.submissionInfo[problem.displayId].ACTime }}<br
/></span>
<el-tooltip effect="dark" placement="top">
<div slot="content">
{{ row.submissionInfo[problem.displayId].specificTime }}
</div>
<span
v-if="row.submissionInfo[problem.displayId].isAC"
class="submission-time"
>{{ row.submissionInfo[problem.displayId].ACTime }}<br />
</span>
</el-tooltip>
<span
class="submission-error"
v-if="
row.submissionInfo[problem.displayId].tryNum == null &&
row.submissionInfo[problem.displayId].errorNum != 0
"
>
(-{{
row.submissionInfo[problem.displayId].errorNum > 0
? row.submissionInfo[problem.displayId].errorNum
: 0
}})
{{
row.submissionInfo[problem.displayId].errorNum > 1
? row.submissionInfo[problem.displayId].errorNum + ' tries'
: row.submissionInfo[problem.displayId].errorNum + ' try'
}}
</span>
<span v-if="row.submissionInfo[problem.displayId].tryNum != null"
><template
v-if="row.submissionInfo[problem.displayId].errorNum > 0"
>
(-{{
{{
row.submissionInfo[problem.displayId].errorNum
}})+</template
>({{ row.submissionInfo[problem.displayId].tryNum
}}+</template
>{{ row.submissionInfo[problem.displayId].tryNum
}}{{
row.submissionInfo[problem.displayId].tryNum > 1
? ' tries'
: ' try'
}})
}}
</span>
</span>
</template>
@ -229,6 +247,7 @@
<Pagination
:total="total"
:page-size.sync="limit"
:page-sizes="[10, 30, 100, 500, 1000, 10000]"
:current.sync="page"
@on-change="getContestRankData"
@on-page-size-change="getContestRankData(1)"
@ -335,13 +354,17 @@ export default {
this.contestID = this.$route.params.contestID;
this.getContestRankData(1);
this.addChartCategory(this.contestProblems);
if (!this.refreshDisabled) {
this.autoRefresh = true;
this.handleAutoRefresh(true);
}
},
methods: {
...mapActions(['getContestProblems']),
getUserTotalSubmit(username) {
getUserACSubmit(username) {
this.$router.push({
name: 'ContestSubmissionList',
query: { username: username },
query: { username: username, status: 0 },
});
},
getUserHomeByUsername(uid, username) {
@ -389,9 +412,15 @@ export default {
let cellClass = {};
Object.keys(info).forEach((problemID) => {
dataRank[i][problemID] = info[problemID];
dataRank[i][problemID].ACTime = time.secondFormat(
dataRank[i][problemID].ACTime
);
if (dataRank[i][problemID].ACTime != null) {
dataRank[i][problemID].errorNum += 1;
dataRank[i][problemID].specificTime = this.parseTimeToSpecific(
dataRank[i][problemID].ACTime
);
dataRank[i][problemID].ACTime = parseInt(
dataRank[i][problemID].ACTime / 60
);
}
let status = info[problemID];
if (status.isFirstAC) {
cellClass[problemID] = 'first-ac';
@ -451,7 +480,7 @@ export default {
this.options.legend.data = users;
this.options.series = seriesData;
},
parseTotalTime(totalTime) {
parseTimeToSpecific(totalTime) {
return time.secondFormat(totalTime);
},
downloadRankCSV() {
@ -536,4 +565,11 @@ a.emphasis:hover {
padding-left: 5px !important;
padding-right: 5px !important;
}
.submission-time {
font-size: 15.6px;
font-family: Roboto, sans-serif;
}
.submission-error {
font-weight: 400;
}
</style>

View File

@ -290,6 +290,10 @@ export default {
mounted() {
this.contestID = this.$route.params.contestID;
this.getContestRankData(1);
if (!this.refreshDisabled) {
this.autoRefresh = true;
this.handleAutoRefresh(true);
}
},
computed: {
contest() {

View File

@ -634,6 +634,7 @@ import {
CONTEST_STATUS,
JUDGE_STATUS_RESERVE,
buildProblemCodeKey,
buildIndividualLanguageAndThemeKey,
RULE_TYPE,
PROBLEM_LEVEL,
} from '@/common/constants';
@ -718,6 +719,15 @@ export default {
vm.theme = problemCode.theme;
});
} else {
let individualLanguageAndTheme = storage.get(
buildIndividualLanguageAndThemeKey()
);
if (individualLanguageAndTheme) {
next((vm) => {
vm.language = individualLanguageAndTheme.language;
vm.theme = individualLanguageAndTheme.theme;
});
}
next();
}
},
@ -966,7 +976,14 @@ export default {
return;
}
// try to load problem template
this.language = this.problemData.languages[0];
if (this.problemData.languages.length != 0) {
if (
!this.language ||
this.problemData.languages.indexOf(this.language) == -1
) {
this.language = this.problemData.languages[0];
}
}
let codeTemplate = this.problemData.codeTemplate;
if (codeTemplate && codeTemplate[this.language]) {
this.code = codeTemplate[this.language];
@ -1320,6 +1337,12 @@ export default {
language: this.language,
theme: this.theme,
});
storage.set(buildIndividualLanguageAndThemeKey(), {
language: this.language,
theme: this.theme,
});
next();
},
watch: {

View File

@ -545,6 +545,9 @@ export default {
let viewData = this.$refs.xTable.getTableData().tableData;
for (let key in submitIds) {
let submitId = parseInt(key);
if (!result[submitId]) {
continue;
}
//
this.submissions[submitIds[key]] = result[submitId];
// viewfip
@ -564,7 +567,7 @@ export default {
delete this.needCheckSubmitIds[key];
}
}
// 180s2s*90
// 600s2s*300
if (
Object.keys(this.needCheckSubmitIds).length == 0 ||
this.checkStatusNum == 300

View File

@ -0,0 +1,234 @@
<template>
<el-row>
<el-card shadow class="training-header">
<div slot="header">
<span class="panel-title">{{ training.title }}</span>
</div>
<el-tag :type="TRAINING_TYPE[training.auth]['color']" effect="dark">
{{ training.auth }}
</el-tag>
</el-card>
<el-tabs @tab-click="tabClick" v-model="route_name" class="card-top">
<el-tab-pane name="TrainingDetails" lazy>
<span slot="label"><i class="el-icon-s-home"></i>&nbsp;训练简介</span>
<el-row :gutter="30">
<el-col :sm="24" :md="8">
<el-card
v-if="passwordFormVisible"
class="password-form-card card-top"
style="text-align:center"
>
<div slot="header">
<span class="panel-title" style="color: #e6a23c;"
><i class="el-icon-warning">
{{ $t('m.Password_Required') }}</i
></span
>
</div>
<h3>
{{ $t('m.To_Enter_Need_Password') }}
</h3>
<el-form>
<el-input
v-model="trainingPassword"
type="password"
:placeholder="$t('m.Enter_the_contest_password')"
@keydown.enter.native="checkPassword"
style="width:70%"
/>
<el-button
type="primary"
@click="checkPassword"
style="margin:5px"
>{{ $t('m.OK') }}</el-button
>
</el-form>
</el-card>
<el-card class="card-top">
<div class="info-rows">
<div>
<span>
<span>训练编号</span>
</span>
<span>
<span>{{ training.rank }}</span>
</span>
</div>
<div>
<span>
<span>训练类型</span>
</span>
<span>
<span
><el-tag
size="medium"
class="category-item"
:style="
'color: #fff;background-color: ' +
training.categoryName +
';background-color: ' +
training.categoryColor
"
>{{ training.categoryName }}</el-tag
></span
>
</span>
</div>
<div>
<span>
<span>总题数</span>
</span>
<span>
<span>{{ training.problemCount }}</span>
</span>
</div>
<div>
<span>
<span>作者</span>
</span>
<span>
<span
><el-link type="info">{{
training.author
}}</el-link></span
>
</span>
</div>
<div>
<span>
<span>最近更新</span>
</span>
<span>
<span>1011-11-11 11:11:11</span>
</span>
</div>
</div>
</el-card>
</el-col>
<el-col :sm="24" :md="16">
<el-card class="card-top">
<div slot="header">
<span class="panel-title">训练简介</span>
</div>
<div
v-html="descriptionHtml"
v-highlight
class="markdown-body"
></div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane
name="ContestProblemList"
lazy
:disabled="contestMenuDisabled"
>
<span slot="label"
><i class="fa fa-list" aria-hidden="true"></i>&nbsp;题目列表</span
>
<transition name="el-zoom-in-bottom">
<router-view v-if="route_name === 'ContestProblemList'"></router-view>
</transition>
</el-tab-pane>
<el-tab-pane
name="ContestSubmissionList"
lazy
:disabled="contestMenuDisabled"
>
<span slot="label"><i class="el-icon-menu"></i>&nbsp;提交列表</span>
<transition name="el-zoom-in-bottom">
<router-view
v-if="route_name === 'ContestSubmissionList'"
></router-view>
</transition>
</el-tab-pane>
<el-tab-pane name="ContestRank" lazy :disabled="contestMenuDisabled">
<span slot="label"
><i class="fa fa-bar-chart" aria-hidden="true"></i
>&nbsp;记录榜单</span
>
<transition name="el-zoom-in-bottom">
<router-view v-if="route_name === 'TrainingRank'"></router-view>
</transition>
</el-tab-pane>
</el-tabs>
</el-row>
</template>
<script>
import { TRAINING_TYPE } from '@/common/constants';
export default {
data() {
return {
training: {
title: '【进阶】一个个进阶者才需要参加的训练',
description: '测试。。。。。。。。。。。。。。。。。',
categoryName: '大佬训练',
categoryColor: '#409eff',
rank: 1000,
problemCount: 10,
auth: 'Private',
author: 'Himit_ZH',
},
route_name: 'TrainingDetails',
TRAINING_TYPE: {},
passwordFormVisible: true,
trainingPassword: '',
};
},
mounted() {
this.TRAINING_TYPE = Object.assign({}, TRAINING_TYPE);
},
computed: {
descriptionHtml() {
if (this.training.description) {
return this.$markDown.render(this.training.description);
}
},
},
};
</script>
<style scoped>
.card-top {
margin-top: 15px;
}
.training-header {
text-align: center;
}
.info-rows > * {
margin-bottom: var(--info-row-margin-bottom, 1em);
display: flex;
align-items: center;
font-size: 16px;
line-height: 1.5;
color: rgba(0, 0, 0, 0.75);
}
.info-rows > * > *:first-child {
flex: 1 0 auto;
text-align: left;
}
.info-rows > :last-child {
margin-bottom: 0;
}
/deep/ .el-card__header {
border-bottom: 0px;
padding-bottom: 0px;
}
/deep/.el-tabs__nav-wrap {
background: #fff;
border-radius: 3px;
}
/deep/.el-tabs--top .el-tabs__item.is-top:nth-child(2) {
padding-left: 20px;
}
/deep/.el-tabs__header {
margin-bottom: 0 !important;
}
</style>

View File

@ -0,0 +1,279 @@
<template>
<el-row>
<el-card>
<section>
<span class="find-training">搜索训练</span>
<vxe-input
v-model="query.keyword"
:placeholder="$t('m.Enter_keyword')"
type="search"
size="medium"
style="width:230px"
@keyup.enter.native="filterByChange"
@search-click="filterByChange"
></vxe-input>
</section>
<section>
<b class="training-category">训练分类</b>
<div>
<el-tag
size="medium"
class="category-item"
v-for="(category, index) in categoryList"
:style="getCategoryBlockColor(category)"
:key="index"
@click="filterByCategory(category.id)"
>{{ category.name }}</el-tag
>
</div>
</section>
</el-card>
<el-card style="margin-top:2em">
<vxe-table
border="inner"
stripe
ref="trainingList"
auto-resize
:data="trainingList"
:loading="loading"
style="font-size: 14px !important;font-weight: 450 !important;"
>
<vxe-table-column
field="rank"
title="编号"
min-width="60"
show-overflow
>
</vxe-table-column>
<vxe-table-column
field="title"
title="标题"
min-width="200"
align="center"
>
<template v-slot="{ row }"
><el-link type="primary" @click="toTraining(row.id)">{{
row.title
}}</el-link>
</template>
</vxe-table-column>
<vxe-table-column
field="auth"
title="权限"
min-width="100"
align="center"
>
<template v-slot="{ row }">
<el-tag :type="TRAINING_TYPE[row.auth]['color']" effect="dark">
{{ row.auth }}
</el-tag>
</template>
</vxe-table-column>
<vxe-table-column
field="categoryName"
title="分类"
min-width="100"
align="center"
>
<template v-slot="{ row }">
<el-tag
size="medium"
class="category-item"
:style="
'background-color: #fff;color: ' +
row.categoryColor +
';border-color: ' +
row.categoryColor +
';'
"
:key="index"
>{{ row.categoryName }}</el-tag
>
</template>
</vxe-table-column>
<vxe-table-column
field="problemCount"
title="题目数"
min-width="80"
align="center"
>
</vxe-table-column>
<vxe-table-column
field="author"
title="作者"
min-width="150"
align="center"
show-overflow
>
<template v-slot="{ row }"
><el-link type="info" @click="goUserHome(row.author)">{{
row.author
}}</el-link>
</template>
</vxe-table-column>
<vxe-table-column
field="gmtModified"
title="最近更新"
min-width="150"
align="center"
show-overflow
>
<template v-slot="{ row }">
{{ row.gmtModified | localtime }}
</template>
</vxe-table-column>
</vxe-table>
</el-card>
<Pagination
:total="total"
:pageSize="limit"
@on-change="getTrainingList"
:current.sync="currentPage"
></Pagination>
</el-row>
</template>
<script>
import api from '@/common/api';
import utils from '@/common/utils';
import myMessage from '@/common/message';
import { TRAINING_TYPE } from '@/common/constants';
const Pagination = () => import('@/components/oj/common/Pagination');
export default {
name: 'TrainingList',
components: {
Pagination,
},
data() {
return {
query: {
keyword: '',
categoryId: null,
},
total: 0,
currentPage: 1,
limit: 15,
categoryList: [],
trainingList: [],
TRAINING_TYPE: {},
loading: false,
};
},
mounted() {
let route = this.$route.query;
this.query.keyword = route.keyword || '';
this.currentPage = parseInt(route.currentPage) || 1;
this.categoryId = route.categoryId || null;
this.TRAINING_TYPE = Object.assign({}, TRAINING_TYPE);
this.getTrainingCategoryList();
this.getTrainingList(1);
myMessage.warning('本训练模块暂未投入使用,正在测试中.....');
this.$alert('本训练模块暂未投入使用,正在测试中.....', '注意', {
confirmButtonText: '确定',
});
},
methods: {
filterByCategory(categoryId) {
this.query.categoryId = categoryId;
this.filterByChange();
},
filterByChange() {
let query = Object.assign({}, this.query);
query.currentPage = this.currentPage;
this.$router.push({
name: 'TrainingList',
query: utils.filterEmptyValue(query),
});
},
getTrainingList(page) {
this.loading = true;
let query = Object.assign({}, this.query);
api.getTrainingList(page, this.limit, query).then(
(res) => {
this.trainingList = res.data.data.records;
this.total = res.data.data.total;
this.loading = false;
},
(err) => {
this.loading = false;
}
);
},
getTrainingCategoryList() {
api.getTrainingCategoryList().then((res) => {
this.categoryList = res.data.data;
});
},
toTraining(trainingID) {
this.$router.push({
name: 'TrainingDetails',
params: { trainingID: trainingID },
});
},
goUserHome(username) {
this.$router.push({
path: '/user-home',
query: { username },
});
},
getCategoryBlockColor(category) {
if (category.id == this.query.categoryId) {
return (
'color: #fff;background-color: ' +
category.color +
';background-color: ' +
category.color +
';'
);
} else {
return (
'background-color: #fff;color: ' +
category.color +
';border-color: ' +
category.color +
';'
);
}
},
},
};
</script>
<style scoped>
section {
display: flex;
min-height: 3em;
margin-bottom: 1em;
align-items: center;
}
.find-training {
margin-right: 1em;
white-space: nowrap;
font-size: 1.7em;
margin-top: 0;
font-family: inherit;
font-weight: bold;
line-height: 1.2;
color: inherit;
}
.training-category {
margin-right: 2.3em;
font-weight: bolder;
white-space: nowrap;
font-size: 16px;
margin-top: 8px;
}
.category-item {
margin-right: 1em;
margin-top: 0.5em;
font-size: 14px;
}
.category-item:hover {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<div class="problem-list">
<vxe-table
border="inner"
stripe
auto-resize
highlight-hover-row
:data="problemList"
align="center"
@cell-click="goTrainingProblem"
>
<vxe-table-column
field="status"
title=""
width="50"
v-if="isAuthenticated && isGetStatusOk"
>
<template v-slot="{ row }">
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
>
<i
class="el-icon-check"
:style="getIconColor(row.myStatus)"
v-if="row.myStatus == 0"
></i>
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
v-else-if="row.myStatus != -10"
></i>
</el-tooltip>
</template>
</vxe-table-column>
<vxe-table-column field="displayId" width="80" title="#">
<template v-slot="{ row }">
<span>{{ row.displayId }}</span>
</template>
</vxe-table-column>
<vxe-table-column
field="displayTitle"
:title="$t('m.Title')"
min-width="200"
></vxe-table-column>
<vxe-table-column
field="difficulty"
:title="$t('m.Level')"
min-width="100"
>
<template v-slot="{ row }">
<span
class="el-tag el-tag--small"
:style="getLevelColor(row.difficulty)"
>{{ PROBLEM_LEVEL[row.difficulty].name }}</span
>
</template>
</vxe-table-column>
<vxe-table-column field="ac" :title="$t('m.AC')" min-width="80">
<template v-slot="{ row }">
<span>
{{ row.ac }}
</span>
</template>
</vxe-table-column>
<vxe-table-column field="total" :title="$t('m.Total')" min-width="80">
<template v-slot="{ row }">
<span>
{{ row.total }}
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="ACRating"
:title="$t('m.AC_Rate')"
min-width="80"
>
<template v-slot="{ row }">
<span>{{ getACRate(row.ac, row.total) }}</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import utils from '@/common/utils';
import { JUDGE_STATUS, PROBLEM_LEVEL } from '@/common/constants';
import api from '@/common/api';
export default {
name: 'TrainingProblemList',
data() {
return {
JUDGE_STATUS: {},
PROBLEM_LEVEL: {},
isGetStatusOk: false,
testcolor: 'rgba(0, 206, 209, 1)',
};
},
mounted() {
this.JUDGE_STATUS = Object.assign({}, JUDGE_STATUS);
this.PROBLEM_LEVEL = Object.assign({}, PROBLEM_LEVEL);
this.getTrainingProblemList();
},
methods: {
getTrainingProblemList() {
this.$store.dispatch('getTrainingProblemList').then((res) => {
if (this.isAuthenticated) {
//
let pidList = [];
if (this.problemList && this.problemList.length > 0) {
for (let index = 0; index < this.problemList.length; index++) {
pidList.push(this.problemList[index].pid);
}
api.getUserProblemStatus(pidList, false).then((res) => {
let result = res.data.data;
for (let index = 0; index < this.problemList.length; index++) {
this.problemList[index]['myStatus'] =
result[this.problemList[index].pid]['status'];
}
this.isGetStatusOk = true;
});
}
}
});
},
goTrainingProblem(event) {
this.$router.push({
name: 'TrainingProblemDetails',
params: {
contestID: this.$route.params.trainingID,
problemID: event.row.displayId,
},
});
},
getACRate(ACCount, TotalCount) {
return utils.getACRate(ACCount, TotalCount);
},
getIconColor(status) {
return (
'font-weight: 600;font-size: 16px;color:' + JUDGE_STATUS[status].rgb
);
},
getLevelColor(difficulty) {
if (difficulty != undefined && difficulty != null) {
return (
'color: #fff !important;background-color:' +
this.PROBLEM_LEVEL[difficulty]['color'] +
' !important;'
);
}
},
},
computed: {
...mapState({
problemList: (state) => state.training.trainingProblemList,
}),
...mapGetters(['isAuthenticated']),
},
};
</script>
<style scoped>
@media screen and (min-width: 1050px) {
/deep/ .vxe-table--body-wrapper {
overflow-x: hidden !important;
}
}
</style>

View File

@ -0,0 +1,484 @@
<template>
<el-card shadow>
<div slot="header">
<span class="panel-title">{{ $t('m.Training_Rank') }}</span>
<span style="float:right;font-size: 20px;">
<el-popover trigger="hover" placement="left-start">
<i class="el-icon-s-tools" slot="reference"></i>
<div id="switches">
<p>
<span>{{ $t('m.Chart') }}</span>
<el-switch v-model="showChart"></el-switch>
</p>
<p>
<span>{{ $t('m.Table') }}</span>
<el-switch v-model="showTable"></el-switch>
</p>
<p>
<span>{{ $t('m.Auto_Refresh') }}(10s)</span>
<el-switch
:disabled="refreshDisabled"
v-model="autoRefresh"
@change="handleAutoRefresh"
></el-switch>
</p>
<template>
<el-button type="primary" size="small" @click="downloadRankCSV">{{
$t('m.Download_as_CSV')
}}</el-button>
</template>
</div>
</el-popover>
</span>
</div>
<div v-show="showChart" class="echarts">
<ECharts :options="options" ref="chart" :autoresize="true"></ECharts>
</div>
<div v-show="showTable">
<vxe-table
round
border
auto-resize
size="medium"
align="center"
:data="dataRank"
:cell-class-name="cellClassName"
:seq-config="{ startIndex: (this.page - 1) * this.limit }"
>
<vxe-table-column
field="id"
type="seq"
width="50"
fixed="left"
></vxe-table-column>
<vxe-table-column
field="username"
fixed="left"
v-if="!isMobileView"
min-width="300"
:title="$t('m.User')"
header-align="center"
align="left"
>
<template v-slot="{ row }">
<avatar
:username="row[training.rankShowName]"
:inline="true"
:size="37"
color="#FFF"
:src="row.avatar"
:title="row[training.rankShowName]"
></avatar>
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="training-username"
><span class="female-flag" v-if="row.gender == 'female'"
>Girl</span
>{{ row[training.rankShowName] }}</span
>
<span class="training-school" v-if="row.school">{{
row.school
}}</span>
</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="username"
v-else
min-width="300"
:title="$t('m.User')"
header-align="center"
align="left"
>
<template v-slot="{ row }">
<avatar
:username="row[training.rankShowName]"
:inline="true"
:size="37"
color="#FFF"
:src="row.avatar"
:title="row[training.rankShowName]"
></avatar>
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="training-username"
><span class="female-flag" v-if="row.gender == 'female'"
>Girl</span
>{{ row[training.rankShowName] }}</span
>
<span class="training-school" v-if="row.school">{{
row.school
}}</span>
</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="realname"
min-width="96"
:title="$t('m.RealName')"
v-if="isTrainingAdmin"
>
</vxe-table-column>
<vxe-table-column
field="rating"
:title="$t('m.AC') + ' / ' + $t('m.Total')"
min-width="80"
>
<template v-slot="{ row }">
<span
>{{ row.ac }} /
<a
@click="getUserTotalSubmit(row.username)"
style="color:rgb(87, 163, 243);"
>{{ row.total }}</a
>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="totalTime"
:title="$t('m.TotalTime')"
min-width="100"
>
<template v-slot="{ row }">
<span>{{ parseTotalTime(row.totalTime) }}</span>
</template>
</vxe-table-column>
<vxe-table-column
min-width="120"
v-for="problem in trainingProblemList"
:key="problem.displayId"
>
<template v-slot:header>
<span
><a
@click="getTrainingProblemById(problem.displayId)"
class="emphasis"
style="color:#495060;"
>{{ problem.displayId }}</a
></span
>
</template>
<template v-slot="{ row }">
<span v-if="row.submissionInfo[problem.displayId]">
<span v-if="row.submissionInfo[problem.displayId].isAC"
>{{ row.submissionInfo[problem.displayId].ACTime }}<br
/></span>
</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
<Pagination
:total="total"
:page-size.sync="limit"
:current.sync="page"
@on-change="getTrainingRankData"
@on-page-size-change="getTrainingRankData(1)"
:layout="'prev, pager, next, sizes'"
></Pagination>
</el-card>
</template>
<script>
import Avatar from 'vue-avatar';
import moment from 'moment';
import { mapActions } from 'vuex';
const Pagination = () => import('@/components/oj/common/Pagination');
import time from '@/common/time';
import utils from '@/common/utils';
export default {
name: 'TrainingRank',
components: {
Pagination,
Avatar,
},
data() {
return {
total: 0,
page: 1,
limit: 30,
autoRefresh: false,
trainingID: '',
dataRank: [],
options: {
title: {
text: this.$i18n.t('m.Top_10_Teams'),
left: 'center',
top: 0,
},
dataZoom: [
{
type: 'inside',
filterMode: 'none',
xAxisIndex: [0],
start: 0,
end: 100,
},
],
toolbox: {
show: true,
feature: {
saveAsImage: { show: true, title: this.$i18n.t('m.save_as_image') },
},
right: '0',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
axis: 'x',
},
},
legend: {
orient: 'horizontal',
x: 'center',
top: '8%',
right: 0,
data: [],
formatter: (value) => {
return utils.breakLongWords(value, 16);
},
textStyle: {
fontSize: 12,
},
},
grid: {
x: 80,
x2: 100,
left: '5%', //canvas
top: '25%',
right: '5%',
bottom: '10%',
},
xAxis: [
{
type: 'time',
splitLine: false,
axisPointer: {
show: true,
snap: true,
},
},
],
yAxis: [
{
type: 'category',
boundaryGap: false,
data: [0],
},
],
series: [],
},
};
},
mounted() {
this.trainingID = this.$route.params.trainingID;
this.getTrainingRankData(1);
this.addChartCategory(this.trainingProblemList);
},
methods: {
...mapActions(['getTrainingProblems']),
getUserTotalSubmit(username) {
this.$router.push({
name: 'TrainingSubmissionList',
query: { username: username },
});
},
getUserHomeByUsername(uid, username) {
this.$router.push({
name: 'UserHome',
query: { username: username, uid: uid },
});
},
getTrainingProblemById(pid) {
this.$router.push({
name: 'TrainingProblemDetails',
params: {
trainingID: this.trainingID,
problemID: pid,
},
});
},
cellClassName({ row, rowIndex, column, columnIndex }) {
if (column.property === 'username' && row.userCellClassName) {
return row.userCellClassName;
}
if (
column.property !== 'id' &&
column.property !== 'rating' &&
column.property !== 'totalTime' &&
column.property !== 'username' &&
column.property !== 'realname'
) {
if (this.isTrainingAdmin) {
return row.cellClassName[
[this.trainingProblemList[columnIndex - 5].displayId]
];
} else {
return row.cellClassName[
[this.trainingProblemList[columnIndex - 4].displayId]
];
}
}
},
applyToTable(data) {
let dataRank = JSON.parse(JSON.stringify(data));
dataRank.forEach((rank, i) => {
let info = rank.submissionInfo;
let cellClass = {};
Object.keys(info).forEach((problemID) => {
dataRank[i][problemID] = info[problemID];
dataRank[i][problemID].ACTime = time.secondFormat(
dataRank[i][problemID].ACTime
);
let status = info[problemID];
if (status.isFirstAC) {
cellClass[problemID] = 'first-ac';
} else if (status.isAC) {
cellClass[problemID] = 'ac';
} else if (status.tryNum != null && status.tryNum > 0) {
cellClass[problemID] = 'try';
} else if (status.errorNum != 0) {
cellClass[problemID] = 'wa';
}
});
dataRank[i].cellClassName = cellClass;
if (dataRank[i].gender == 'female') {
dataRank[i].userCellClassName = 'bg-female';
}
});
this.dataRank = dataRank;
},
addChartCategory(trainingProblemList) {
let category = [];
for (let i = 0; i <= trainingProblemList.length; ++i) {
category.push(i);
}
this.options.yAxis[0].data = category;
},
applyToChart(rankData) {
let [users, seriesData] = [[], []];
rankData.forEach((rank) => {
users.push(rank[this.training.rankShowName]);
let info = rank.submissionInfo;
// AC
let timeData = [];
Object.keys(info).forEach((problemID) => {
if (info[problemID].isAC) {
timeData.push(info[problemID].ACTime);
}
});
timeData.sort((a, b) => {
return a - b;
});
let data = [];
data.push([this.training.startTime, 0]);
for (let [index, value] of timeData.entries()) {
let realTime = moment(this.training.startTime)
.add(value, 'seconds')
.format();
data.push([realTime, index + 1]);
}
seriesData.push({
name: rank[this.training.rankShowName],
type: 'line',
data,
});
});
this.options.legend.data = users;
this.options.series = seriesData;
},
parseTotalTime(totalTime) {
return time.secondFormat(totalTime);
},
downloadRankCSV() {
utils.downloadFile(
`/api/file/download-training-rank?cid=${
this.$route.params.trainingID
}&forceRefresh=${this.forceUpdate ? true : false}`
);
},
},
watch: {
trainingProblemList(newVal, OldVal) {
if (newVal.length != 0) {
this.addChartCategory(this.trainingProblemList);
}
},
},
computed: {
training() {
return this.$store.state.training.training;
},
isMobileView() {
return window.screen.width < 768;
},
},
};
</script>
<style scoped>
.echarts {
margin: 20px auto;
height: 400px;
width: 100%;
}
/deep/.el-card__body {
padding: 20px !important;
padding-top: 0px !important;
}
.screen-full {
margin-right: 8px;
}
#switches p {
margin-top: 5px;
}
#switches p:first-child {
margin-top: 0;
}
#switches p span {
margin-left: 8px;
margin-right: 4px;
}
.vxe-cell p,
.vxe-cell span {
margin: 0;
padding: 0;
}
/deep/.vxe-table .vxe-body--column {
line-height: 20px !important;
padding: 0 !important;
}
@media screen and (max-width: 768px) {
/deep/.el-card__body {
padding: 0 !important;
}
}
a.emphasis {
color: #495060 !important;
}
a.emphasis:hover {
color: #2d8cf0 !important;
}
/deep/.vxe-body--column {
min-width: 0;
height: 48px;
box-sizing: border-box;
text-align: left;
text-overflow: ellipsis;
vertical-align: middle;
}
/deep/.vxe-table .vxe-cell {
padding-left: 5px !important;
padding-right: 5px !important;
}
</style>

View File

@ -5,7 +5,7 @@ const CompressionWebpackPlugin = require('compression-webpack-plugin'); // 开
const isProduction = process.env.NODE_ENV === 'production';
// 本地环境是否需要使用cdn
const devNeedCdn = true
const devNeedCdn = true;
// cdn链接
const cdn = {

View File

@ -378,20 +378,22 @@ IF NOT EXISTS (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `training_problem` (
`id` bigint unsigned NOT NULL,
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL COMMENT '训练id',
`pid` bigint unsigned NOT NULL COMMENT '题目id',
`rank` int DEFAULT '0' COMMENT '排序用',
`rank` int DEFAULT '0',
`display_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `tid` (`tid`),
KEY `pid` (`pid`),
KEY `display_id` (`display_id`),
CONSTRAINT `training_problem_ibfk_1` FOREIGN KEY (`tid`) REFERENCES `training` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_problem_ibfk_2` FOREIGN KEY (`pid`) REFERENCES `problem` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
CONSTRAINT `training_problem_ibfk_2` FOREIGN KEY (`pid`) REFERENCES `problem` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_problem_ibfk_3` FOREIGN KEY (`display_id`) REFERENCES `problem` (`problem_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `training_record` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL,
@ -454,3 +456,71 @@ CALL Add_training_table;
DROP PROCEDURE Add_training_table;
/*
* 2021.12.05 contest增加auto_real_rank比赛结束是否自动解除封榜,
*/
DROP PROCEDURE
IF EXISTS contest_Add_auto_real_rank;
DELIMITER $$
CREATE PROCEDURE contest_Add_auto_real_rank; ()
BEGIN
IF NOT EXISTS (
SELECT
1
FROM
information_schema.`COLUMNS`
WHERE
table_name = 'conetst'
AND column_name = 'auto_real_rank'
) THEN
ALTER TABLE `hoj`.`contest` ADD COLUMN `auto_real_rank` BOOLEAN DEFAULT 1 NULL COMMENT '比赛结束是否自动解除封榜,自动转换成真实榜单';
DROP TABLE `hoj`.`training_problem`;
DROP TABLE `hoj`.`training_record`;
CREATE TABLE `training_problem` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL COMMENT '训练id',
`pid` bigint unsigned NOT NULL COMMENT '题目id',
`rank` int DEFAULT '0',
`display_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `tid` (`tid`),
KEY `pid` (`pid`),
KEY `display_id` (`display_id`),
CONSTRAINT `training_problem_ibfk_1` FOREIGN KEY (`tid`) REFERENCES `training` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_problem_ibfk_2` FOREIGN KEY (`pid`) REFERENCES `problem` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_problem_ibfk_3` FOREIGN KEY (`display_id`) REFERENCES `problem` (`problem_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `training_record` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL,
`tpid` bigint unsigned NOT NULL,
`pid` bigint unsigned NOT NULL,
`uid` varchar(255) NOT NULL,
`submit_id` bigint unsigned NOT NULL,
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `tid` (`tid`),
KEY `tpid` (`tpid`),
KEY `pid` (`pid`),
KEY `uid` (`uid`),
KEY `submit_id` (`submit_id`),
CONSTRAINT `training_record_ibfk_1` FOREIGN KEY (`tid`) REFERENCES `training` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_record_ibfk_2` FOREIGN KEY (`tpid`) REFERENCES `training_problem` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_record_ibfk_3` FOREIGN KEY (`pid`) REFERENCES `problem` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_record_ibfk_4` FOREIGN KEY (`uid`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_record_ibfk_5` FOREIGN KEY (`submit_id`) REFERENCES `judge` (`submit_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
END
IF ; END$$
DELIMITER ;
CALL contest_Add_auto_real_rank; ;
DROP PROCEDURE contest_Add_auto_real_rank;;

View File

@ -142,6 +142,7 @@ CREATE TABLE `contest` (
`duration` bigint(20) DEFAULT NULL COMMENT '比赛时长(s)',
`seal_rank` tinyint(1) DEFAULT '0' COMMENT '是否开启封榜',
`seal_rank_time` datetime DEFAULT NULL COMMENT '封榜起始时间,一直到比赛结束,不刷新榜单',
`auto_real_rank` tinyint(1) DEFAULT '1' COMMENT '比赛结束是否自动解除封榜,自动转换成真实榜单',
`status` int(11) DEFAULT NULL COMMENT '-1为未开始0为进行中1为已结束',
`visible` tinyint(1) DEFAULT '1' COMMENT '是否可见',
`open_print` tinyint(1) DEFAULT '0' COMMENT '是否打开打印功能',
@ -885,17 +886,20 @@ CREATE TABLE `training_category` (
DROP TABLE IF EXISTS `training_problem`;
CREATE TABLE `training_problem` (
`id` bigint unsigned NOT NULL,
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL COMMENT '训练id',
`pid` bigint unsigned NOT NULL COMMENT '题目id',
`rank` int DEFAULT '0' COMMENT '排序用',
`rank` int DEFAULT '0',
`display_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `tid` (`tid`),
KEY `pid` (`pid`),
KEY `display_id` (`display_id`),
CONSTRAINT `training_problem_ibfk_1` FOREIGN KEY (`tid`) REFERENCES `training` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_problem_ibfk_2` FOREIGN KEY (`pid`) REFERENCES `problem` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
CONSTRAINT `training_problem_ibfk_2` FOREIGN KEY (`pid`) REFERENCES `problem` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `training_problem_ibfk_3` FOREIGN KEY (`display_id`) REFERENCES `problem` (`problem_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `training_record` */