训练模块80%更新,优化比赛排行榜
This commit is contained in:
parent
e358a78a8c
commit
85c2f96377
|
@ -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'
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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
|
||||
---
|
|
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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`即可。
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
@ -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` 即可。
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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("回复失败,请重新尝试!");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())) {
|
||||
|
|
|
@ -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, "获取最新判题数据成功!");
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
<!-- 子查询 :为了防止分页总数据数出错-->
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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, "查询成功!");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ public class Training implements Serializable {
|
|||
private Boolean status;
|
||||
|
||||
@ApiModelProperty(value = "编号,升序排序")
|
||||
@TableField("`rank`")
|
||||
private Integer rank;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
|
|
|
@ -40,6 +40,7 @@ public class TrainingProblem implements Serializable {
|
|||
private String displayId;
|
||||
|
||||
@ApiModelProperty(value = "排序用")
|
||||
@TableField("`rank`")
|
||||
private Integer rank;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -2,6 +2,7 @@ export const m = {
|
|||
// /components/oj/common/NavBar.vue 导航栏
|
||||
NavBar_Home: '首页',
|
||||
NavBar_Problem: '题目',
|
||||
NavBar_Training: '训练',
|
||||
NavBar_Contest: '比赛',
|
||||
NavBar_Status: '评测',
|
||||
NavBar_Rank: '排名',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
() => {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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];
|
||||
// 更新view中的结果,f分数,耗时,空间消耗,判题机ip
|
||||
|
@ -564,7 +567,7 @@ export default {
|
|||
delete this.needCheckSubmitIds[key];
|
||||
}
|
||||
}
|
||||
// 当前提交列表的提交都判题结束或者检查结果180s(2s*90)还没判题结束,为了避免无用请求加重服务器负担,直接停止检查的请求。
|
||||
// 当前提交列表的提交都判题结束或者检查结果600s(2s*300)还没判题结束,为了避免无用请求加重服务器负担,直接停止检查的请求。
|
||||
if (
|
||||
Object.keys(this.needCheckSubmitIds).length == 0 ||
|
||||
this.checkStatusNum == 300
|
||||
|
|
|
@ -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> 训练简介</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> 题目列表</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> 提交列表</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
|
||||
> 记录榜单</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 = {
|
||||
|
|
|
@ -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;;
|
||||
|
|
|
@ -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` */
|
||||
|
|
Loading…
Reference in New Issue