前端管理端页面基本完成

This commit is contained in:
Himit_ZH 2020-12-01 23:10:21 +08:00
parent 3b18df5563
commit acbf59ad77
30 changed files with 2509 additions and 423 deletions

View File

@ -16,6 +16,7 @@
| 2020-11-22 | 前端比赛首页,比赛题目列表,比赛排行榜,比赛公告,首页布局调整 | Himit_ZH |
| 2020-11-24 | 介绍页,导航栏移动端优化,首页优化,公告栏优化 | Himit_ZH |
| 2020-11-28 | 前端项目重构,加入管理端部分页面,增加case表 | Himit_ZH |
| 2020-12-01 | 前端管理端基本完成,准备开始前后端接口对接与测试 | Himit_ZH |
# 二、系统架构
@ -175,16 +176,19 @@ problem表
| id | long | primary key | auto_increment 1000开始 |
| title | String | | 题目 |
| author | String | | 默认可为无 |
| type | int | | 题目类型 0为ACM,1为OI |
| time_limit | int | | 时间限制(ms)默认为c/c++限制,其它语言为2倍 |
| memory_limit | int | | 空间限制(k)默认为c/c++限制,其它语言为2倍 |
| memory_limit | int | | 空间限制(mb)默认为c/c++限制,其它语言为2倍 |
| description | String | | 内容描述 |
| input | String | | 输入描述 |
| output | String | | 输出描述 |
| sample_input | Srting | | 输入样例,多样例用(#)隔开 |
| sample_output | String | | 输出样例 |
| source | int | | 题目来源比赛id默认为hoj,可能为爬虫vj |
| comment | String | | 备注 |
| difficulty | int | | 题目难度0简单1中等2困难 |
| hint | String | | 备注 提醒 |
| auth | int | | 默认为1公开2为私有3为比赛中。 |
| code_share | boolean | | 该题目对应的相关提交代码,用户是否可用分享 |
| gmt_create | datetime | | 创建时间 |
| gmt_modified | datetime | | 修改时间 |
@ -322,25 +326,69 @@ jugdeCase表 评测单个样例结果表
## 比赛模块
更新比赛状态的存储过程
```sql
DELIMITER |
DROP PROCEDURE IF EXISTS contest_status |
CREATE PROCEDURE contest_status()
BEGIN
UPDATE contest
SET STATUS = (
CASE
WHEN NOW() < start_time THEN STATUS = -1
WHEN NOW() >= start_time AND NOW()<end_time THEN STATUS = 0
WHEN NOW() > end_time THEN STATUS = 1
END);
END
|
```
设置定时器
```sql
SET GLOBAL event_scheduler = 1; // 开启定时器
CREATE EVENT IF NOT EXISTS contest_event
ON SCHEDULE EVERY 1 SECOND // 每秒执行一次
ON COMPLETION PRESERVE
DO CALL contest_status(); // 调用存储过程
```
开启或关闭定时器
```sql
ALTER EVENT contest_event ON COMPLETION PRESERVE ENABLE; -- 开启事件
ALTER EVENT contest_event ON COMPLETION PRESERVE DISABLE; -- 关闭事件
```
contest表
| 列名 | 实体属性类型 | 键 | 备注 |
| ------------ | ------------ | ---- | ----------------------------------------------------- |
| id | long | 主键 | auto_increment 1000起步 |
| uid | String | 外键 | 创建者id |
| title | String | | 比赛标题 |
| type | int | | Acm赛制或者Rating |
| source | int | | 比赛来源原创为0克隆赛为比赛id |
| auth | int | | 0为公开赛1为私有赛有密码3为保护赛有密码。 |
| pwd | string | | 比赛密码 |
| start_time | datetime | | 开始时间 |
| end_time | datetime | | 结束时间 |
| duration | int | | 比赛时长(分) |
| explain | Srting | | 比赛说明 |
| gmt_create | datetime | | 创建时间 |
| gmt_modified | datetime | | 修改时间 |
| 列名 | 实体属性类型 | 键 | 备注 |
| -------------- | ------------ | ---- | ----------------------------------------------------- |
| id | long | 主键 | auto_increment 1000起步 |
| uid | String | 外键 | 创建者id |
| title | String | | 比赛标题 |
| type | int | | Acm赛制或者Rating |
| source | int | | 比赛来源原创为0克隆赛为比赛id |
| auth | int | | 0为公开赛1为私有赛有密码3为保护赛有密码。 |
| pwd | string | | 比赛密码 |
| start_time | datetime | | 开始时间 |
| end_time | datetime | | 结束时间 |
| duration | long | | 比赛时长s |
| explain | Srting | | 比赛说明 |
| seal_rank | boolean | | 是否开启封榜 |
| seal_rank_time | datetime | | 封榜起始时间,一直到比赛结束,不刷新榜单。 |
| status | int | | -1为未开始0为进行中1为已结束 |
| gmt_create | datetime | | 创建时间 |
| gmt_modified | datetime | | 修改时间 |

View File

@ -2,7 +2,7 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.hcode.hoj.dao.ProblemMapper">
<select id="getProblemList" resultType="top.hcode.hoj.pojo.vo.ProblemVo" useCache="false">
select p.id,title,author,source,total,ac,mle,tle,re,pe,ce,wa,se,score from problem p,problem_count pc
select p.id,title,author,type,source,total,ac,mle,tle,re,pe,ce,wa,se,score from problem p,problem_count pc
<where>
auth = 1 and p.id = pc.pid
<if test="pid!=0">

View File

@ -2,7 +2,7 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.hcode.hoj.dao.ProblemMapper">
<select id="getProblemList" resultType="top.hcode.hoj.pojo.vo.ProblemVo" useCache="false">
select p.id,title,author,source,total,ac,mle,tle,re,pe,ce,wa,se,score from problem p,problem_count pc
select p.id,title,author,type,source,total,ac,mle,tle,re,pe,ce,wa,se,score from problem p,problem_count pc
<where>
auth = 1 and p.id = pc.pid
<if test="pid!=0">

View File

@ -48,7 +48,7 @@ public class Contest implements Serializable {
@ApiModelProperty(value = "比赛来源原创为0克隆赛为比赛id")
private Integer source;
@ApiModelProperty(value = "0为公开赛1为私有赛有密码2为报名")
@ApiModelProperty(value = "0为公开赛1为私有赛有密码2为保护")
private Integer auth;
@ApiModelProperty(value = "比赛密码")
@ -60,8 +60,17 @@ public class Contest implements Serializable {
@ApiModelProperty(value = "结束时间")
private Date endTime;
@ApiModelProperty(value = "比赛时长(分)")
private Integer duration;
@ApiModelProperty(value = "比赛时长s")
private Long duration;
@ApiModelProperty(value = "是否开启封榜")
private Boolean sealRank;
@ApiModelProperty(value = "封榜起始时间,一直到比赛结束,不刷新榜单")
private Date sealRankTime;
@ApiModelProperty(value = "-1为未开始0为进行中1为已结束")
private Integer status;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;

View File

@ -32,14 +32,19 @@ public class Problem implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "题目")
private String title;
@ApiModelProperty(value = "作者")
private String author;
@ApiModelProperty(value = "0为ACM,1为OI")
private Integer type;
@ApiModelProperty(value = "单位ms")
private Integer timeLimit;
@ApiModelProperty(value = "单位kb")
@ApiModelProperty(value = "单位mb")
private Integer memoryLimit;
@ApiModelProperty(value = "描述")
@ -63,12 +68,15 @@ public class Problem implements Serializable {
@ApiModelProperty(value = "题目难度")
private String difficulty;
@ApiModelProperty(value = "备注")
private String comment;
@ApiModelProperty(value = "备注,提醒")
private String hint;
@ApiModelProperty(value = "默认为1公开2为私有3为比赛中")
private Integer auth;
@ApiModelProperty(value = "该题目对应的相关提交代码,用户是否可用分享")
private Boolean codeShare;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;

View File

@ -96,13 +96,13 @@ samp {
a {
text-decoration: none;
background-color: transparent;
color: #495060!important;
color: #495060;
outline: 0;
cursor: pointer;
transition: color 0.2s ease;
}
a:hover {
color: #2196f3;
color: #2196f3!important;
}
.drop-menu {
padding-top: 7px;
@ -165,6 +165,10 @@ a:hover {
font-size: 12px !important;
font-weight: 500 !important;
}
.row--hover{
cursor: pointer;
background-color: #ebf7ff!important;
}
.vxe-table .vxe-body--column:not(.col--ellipsis),
.vxe-table .vxe-footer--column:not(.col--ellipsis),
.vxe-table .vxe-header--column:not(.col--ellipsis) {

View File

@ -16,11 +16,19 @@ function parseRole(num){
return '用户'
}
}
function parseContestType(num){
if(num==0){
return 'ACM'
}else if(num==1){
return 'OI'
}
}
export default {
submissionMemory: utils.submissionMemoryFormat,
submissionTime: utils.submissionTimeFormat,
localtime: time.utcToLocal,
fromNow: fromNow,
parseContestType:parseContestType,
parseRole:parseRole
}

View File

@ -1,7 +1,7 @@
<template>
<div class="accordion">
<header>
<h2>{{title}}</h2>
<span class="title">{{ title }}</span>
<div class="header_right">
<slot name="header"></slot>
</div>
@ -9,68 +9,69 @@
<div class="body" v-show="isOpen">
<slot></slot>
</div>
<footer @click="isOpen = !isOpen"><i :class="{'rotate': !isOpen}" class="el-icon-caret-top"></i></footer>
<footer @click="isOpen = !isOpen">
<i :class="{ rotate: !isOpen }" class="el-icon-caret-top"></i>
</footer>
</div>
</template>
<script>
export default{
name: 'Accordion',
props: {
title: {
type: String,
required: true
}
export default {
name: "Accordion",
props: {
title: {
type: String,
required: true,
},
data () {
return {
isOpen: true
}
}
}
},
data() {
return {
isOpen: true,
};
},
};
</script>
<style lang="less" scoped>
.accordion{
<style scoped>
.tit
.accordion {
border: 1px solid #eaeefb;
header{
position: relative;
h2{
font-size: 14px;
margin: 0 0 0 10px;
line-height: 50px;
}
.header_right{
right: 5px;
top: 5px;
position: absolute;
}
}
.body{
background-color: #f9fafc;
border-top: 1px solid #eaeefb;
clear: both;
overflow: hidden;
padding: 15px 10px;
}
footer{
border-top: 1px solid #eaeefb;
height: 36px;
box-sizing: border-box;
background-color: #fff;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
text-align: center;
margin-top: -1px;
color: #d3dce6;
cursor: pointer;
transition: .2s;
&:hover{
background-color: #f9fafc;
}
.rotate{
transform: rotate(180deg);
}
}
}
.accordion header {
position: relative;
}
.title {
font-size: 14px;
margin: 0 0 0 10px;
line-height: 50px;
}
.header_right {
float: right;
}
.body {
background-color: #f9fafc;
border-top: 1px solid #eaeefb;
clear: both;
overflow: hidden;
padding: 15px 10px;
}
footer {
border-top: 1px solid #eaeefb;
height: 36px;
box-sizing: border-box;
background-color: #fff;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
text-align: center;
margin-top: -1px;
color: #d3dce6;
cursor: pointer;
transition: 0.2s;
}
footer:hover {
background-color: #f9fafc;
}
.rotate {
transform: rotate(180deg);
}
</style>

View File

@ -16,7 +16,7 @@
mode: 'text/x-csrc',
lineNumbers: true,
lineWrapping: false,
theme: 'solarized',
theme: 'monokai',
tabSize: 4,
line: true,
foldGutter: true,

View File

@ -0,0 +1,113 @@
<template>
<div>
<vxe-input v-model="keyword" placeholder="Enter keyword"
type="search" size="medium" @search-click="filterByKeyword" style="margin-bottom:10px"></vxe-input>
<vxe-table
:data="problems" :loading="loading"
auto-resize
stripe
align="center"
>
<vxe-table-column
title="ID"
min-width="100"
field="id">
</vxe-table-column>
<vxe-table-column
min-width="150"
title="Title"
field="title">
</vxe-table-column>
<vxe-table-column
title="option"
align="center"
min-width="100">
<template v-slot="{row}">
<el-tooltip effect="dark" content="添加该题目到比赛中" placement="top">
<el-button icon="el-icon-plus" size="mini" @click.native="handleAddProblem(row.id)" type="primary">
</el-button>
</el-tooltip>
</template>
</vxe-table-column>
</vxe-table>
<el-pagination
class="page"
layout="prev, pager, next"
@current-change="getPublicProblem"
:page-size="limit"
:total="total">
</el-pagination>
</div>
</template>
<script>
import api from '@/common/api'
export default {
name: 'add-problem-from-public',
fields: ['contestID'],
data () {
return {
page: 1,
limit: 10,
total: 0,
loading: false,
problems: [
{id:1000,title:'测试'}
],
contest: {},
keyword: ''
}
},
mounted () {
// api.getContest(this.contestID).then(res => {
// this.contest = res.data.data
// this.getPublicProblem()
// }).catch(() => {
// })
},
methods: {
getPublicProblem (page) {
this.loading = true
let params = {
keyword: this.keyword,
offset: (page - 1) * this.limit,
limit: this.limit,
rule_type: this.contest.rule_type
}
api.getProblemList(params).then(res => {
this.loading = false
this.total = res.data.data.total
this.problems = res.data.data.results
}).catch(() => {
})
},
handleAddProblem (problemID) {
this.$prompt('Please input display id for the contest problem', 'confirm').then(({value}) => {
let data = {
problem_id: problemID,
contest_id: this.contestID,
display_id: value
}
api.addProblemFromPublic(data).then(() => {
this.$emit('on-change')
}, () => {
})
}, () => {
})
},
filterByKeyword(){
this.getPublicProblem(this.page)
}
},
}
</script>
<style scoped>
.page {
margin-top: 20px;
text-align: right
}
</style>

View File

@ -1,18 +0,0 @@
<template>
<div class="breadcrumb">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">Home page</el-breadcrumb-item>
<el-breadcrumb-item><slot name="topNavName">PLEASE OVERIDE ME</slot></el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
<style scoped>
.breadcrumb {
margin: 10px;
margin-right: 25px;
margin-bottom: 20px;
padding: 15px;
background-color: #fff;
}
</style>

View File

@ -65,6 +65,11 @@ export default {
text-align: center;
vertical-align: middle;
}
@media screen and (max-width: 1080px) {
.info-card {
margin-right: 0;
}
}
.info-card-container {
height: 100%;
align-items: center;

View File

@ -137,61 +137,67 @@
</mu-appbar>
<mu-drawer :open.sync="opendrawer" :docked="false" :right="false">
<mu-list @change="opendrawer = false">
<mu-list-item button to="/home">
<mu-list toggle-nested>
<mu-list-item button to="/home" @click="opendrawer =!opendrawer">
<mu-list-item-action>
<i class="el-icon-s-home" style="font-size: 20px;"></i>
<mu-icon value="home" size="24"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Home</mu-list-item-title>
</mu-list-item>
<mu-list-item button to="/problem">
<mu-list-item button to="/problem" @click="opendrawer =!opendrawer">
<mu-list-item-action>
<i class="el-icon-s-grid" style="font-size: 20px;"></i>
<mu-icon value=":el-icon-s-grid" size="24"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Problem</mu-list-item-title>
</mu-list-item>
<mu-list-item button to="/contest">
<mu-list-item button to="/contest" @click="opendrawer =!opendrawer">
<mu-list-item-action>
<i class="el-icon-trophy" style="font-size: 20px;"></i>
<mu-icon value=":el-icon-trophy" size="24"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Contest</mu-list-item-title>
</mu-list-item>
<mu-list-item button to="/status">
<mu-list-item button to="/status" @click="opendrawer =!opendrawer">
<mu-list-item-action>
<i class="el-icon-s-marketing" style="font-size: 20px;"></i>
<mu-icon value=":el-icon-s-marketing" size="24"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Status</mu-list-item-title>
</mu-list-item>
<mu-list-item button to="/acm-rank">
<mu-list-item button :ripple="false"
nested :open="openSideMenu === 'rank'" @toggle-nested="openSideMenu = arguments[0] ? 'rank' : ''">
<mu-list-item-action>
<i class="el-icon-s-data" style="font-size: 20px;"></i>
<mu-icon value=":el-icon-s-data" size="24"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Rank-ACM</mu-list-item-title>
<mu-list-item-title>Rank</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="/acm-rank" @click="opendrawer =!opendrawer">
<mu-list-item-title>ACM Rank</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" slot="nested" to="/oi-rank" @click="opendrawer =!opendrawer">
<mu-list-item-title>OI Rank</mu-list-item-title>
</mu-list-item>
</mu-list-item>
<mu-list-item button to="/oi-rank">
<mu-list-item button :ripple="false"
nested :open="openSideMenu === 'about'" @toggle-nested="openSideMenu = arguments[0] ? 'about' : ''">
<mu-list-item-action>
<i class="el-icon-data-analysis" style="font-size: 20px;"></i>
<mu-icon value=":el-icon-info" size="24"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Rank-OI</mu-list-item-title>
</mu-list-item>
<mu-list-item button to="/introduction" style="font-size: 20px;">
<mu-list-item-title>About</mu-list-item-title>
<mu-list-item-action>
<i class="el-icon-info"></i>
<mu-icon class="toggle-icon" size="24" value="keyboard_arrow_down"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>About-Introduction</mu-list-item-title>
</mu-list-item>
<mu-list-item button to="/developer" style="font-size: 20px;">
<mu-list-item-action>
<i class="el-icon-user-solid"></i>
</mu-list-item-action>
<mu-list-item-title>About-Developer</mu-list-item-title>
<mu-list-item button :ripple="false" slot="nested" to="/introduction" @click="opendrawer =!opendrawer">
<mu-list-item-title>Introduction</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" slot="nested" to="/developer" @click="opendrawer =!opendrawer">
<mu-list-item-title>Developer</mu-list-item-title>
</mu-list-item>
</mu-list-item>
</mu-list>
@ -239,6 +245,7 @@ export default {
mobileNar: false,
opendrawer:false,
openusermenu:false,
openSideMenu:'',
imgUrl: require("@/assets/logo.png"),
};
},

View File

@ -6,11 +6,16 @@ import Dashboard from '@/views/admin/Dashboard'
import User from '@/views/admin/general/User'
import Announcement from '@/views/admin/general/Announcement'
import SystemConfig from '@/views/admin/general/SystemConfig'
import PruneTestCase from '@/views/admin/general/PruneTestCase'
import DeleteTestCase from '@/views/admin/general/DeleteTestCase'
import ProblemList from '@/views/admin/problem/ProblemList'
import Problem from '@/views/admin/problem/Problem'
import ProblemImportAndExport from '@/views/admin/problem/ImportAndExport'
import Contest from '@/views/admin/contest/Contest'
import ContestList from '@/views/admin/contest/ContestList'
const adminRoutes= [
{
path: '/admin/login',
name: 'login',
name: 'admin-login',
component: Login
},
{
@ -19,88 +24,88 @@ const adminRoutes= [
children: [
{
path: '',
name: 'dashboard',
name: 'admin-dashboard',
component: Dashboard
},
{
path: 'user',
name: 'user',
name: 'admin-user',
component: User
},
{
path: 'announcement',
name: 'announcement',
name: 'admin-announcement',
component: Announcement
},
{
path: 'conf',
name: 'conf',
name: 'admin-conf',
component: SystemConfig
},
{
path: 'prune-test-case',
name: 'prune-test-case',
component: PruneTestCase
path: 'delete-test-case',
name: 'admin-delete-test-case',
component: DeleteTestCase
},
// {
// path: '/problems',
// name: 'problem-list',
// component: ProblemList
// },
// {
// path: '/problem/create',
// name: 'create-problem',
// component: Problem
// },
// {
// path: '/problem/edit/:problemId',
// name: 'edit-problem',
// component: Problem
// },
// {
// path: '/problem/batch_ops',
// name: 'problem_batch_ops',
// component: ProblemImportOrExport
// },
// {
// path: '/contest/create',
// name: 'create-contest',
// component: Contest
// },
// {
// path: '/contest',
// name: 'contest-list',
// component: ContestList
// },
// {
// path: '/contest/:contestId/edit',
// name: 'edit-contest',
// component: Contest
// },
// {
// path: '/contest/:contestId/announcement',
// name: 'contest-announcement',
// component: Announcement
// },
// {
// path: '/contest/:contestId/problems',
// name: 'contest-problem-list',
// component: ProblemList
// },
// {
// path: '/contest/:contestId/problem/create',
// name: 'create-contest-problem',
// component: Problem
// },
// {
// path: '/contest/:contestId/problem/:problemId/edit',
// name: 'edit-contest-problem',
// component: Problem
// }
{
path: 'problems',
name: 'admin-problem-list',
component: ProblemList
},
{
path: 'problem/create',
name: 'admin-create-problem',
component: Problem
},
{
path: 'problem/edit/:problemId',
name: 'admin-edit-problem',
component: Problem
},
{
path: 'problem/batch-operation',
name: 'admin-problem_batch_operation',
component: ProblemImportAndExport
},
{
path: 'contest/create',
name: 'admin-create-contest',
component: Contest
},
{
path: 'contest',
name: 'admin-contest-list',
component: ContestList
},
{
path: 'contest/:contestId/edit',
name: 'admin-edit-contest',
component: Contest
},
{
path: 'contest/:contestId/announcement',
name: 'admin-contest-announcement',
component: Announcement
},
{
path: 'contest/:contestId/problems',
name: 'admin-contest-problem-list',
component: ProblemList
},
{
path: 'contest/:contestId/problem/create',
name: 'admin-create-contest-problem',
component: Problem
},
{
path: 'contest/:contestId/problem/:problemId/edit',
name: 'admin-edit-contest-problem',
component: Problem
}
]
},
{
path: '*', redirect: '/login'
path: '/admin/*', redirect: '/admin/login'
}
]

View File

@ -77,7 +77,7 @@
</el-col>
</el-row>
<el-card>
<el-card style="margin-top:10px">
<div slot="header">
<span class="panel-title home-title">Judger Service</span>
</div>
@ -295,11 +295,6 @@ export default {
justify-content: flex-start;
flex-wrap: wrap;
}
@media screen and (max-width: 1080px) {
.info-container{
margin-top: 10px;
}
}
.info-container .info-item {
flex: 1 0 auto;
min-width: 200px;

View File

@ -1,60 +1,96 @@
<template>
<div class="admin-container">
<div v-if="!mobileNar">
<el-menu class="vertical_menu"
:router="true" :default-active="currentPath" >
<div class="logo">
<img src="@/assets/logo.png" alt="oj admin"/>
</div>
<el-menu-item index="/admin/">
<i class="fa fa-tachometer" aria-hidden="true"></i>Dashboard
</el-menu-item>
<!-- <el-submenu v-if="isSuperAdmin" index="general"> -->
<el-submenu index="general">
<template slot="title"><i class="el-icon-menu"></i>General</template>
<el-menu-item index="/admin/user">User</el-menu-item>
<el-menu-item index="/admin/announcement">Announcement</el-menu-item>
<el-menu-item index="/admin/conf">System Config</el-menu-item>
<el-menu-item index="/admin/prune-test-case">Prune Test Case</el-menu-item>
</el-submenu>
<!-- <el-submenu index="problem" v-if="hasProblemPermission"> -->
<el-menu
class="vertical_menu"
:router="true"
:default-active="currentPath"
>
<div class="logo">
<img src="@/assets/logo.png" alt="oj admin" />
</div>
<el-menu-item index="/admin/">
<i class="fa fa-tachometer" aria-hidden="true"></i>Dashboard
</el-menu-item>
<!-- <el-submenu v-if="isSuperAdmin" index="general"> -->
<el-submenu index="general">
<template slot="title"><i class="el-icon-menu"></i>General</template>
<el-menu-item index="/admin/user">User</el-menu-item>
<el-menu-item index="/admin/announcement">Announcement</el-menu-item>
<el-menu-item index="/admin/conf">System Config</el-menu-item>
<el-menu-item index="/admin/delete-test-case"
>Delete Test Case</el-menu-item
>
</el-submenu>
<!-- <el-submenu index="problem" v-if="hasProblemPermission"> -->
<el-submenu index="problem">
<template slot="title"><i class="fa fa-bars" aria-hidden="true"></i>Problem</template>
<el-menu-item index="/admin/problems">Problem List</el-menu-item>
<el-menu-item index="/admin/problem/create">Create Problem</el-menu-item>
<el-menu-item index="/admin/problem/batch_ops">Export&Import Problem</el-menu-item>
</el-submenu>
<el-submenu index="contest">
<template slot="title"><i class="fa fa-trophy" aria-hidden="true"></i>Contest</template>
<el-menu-item index="/admin/contest">Contest List</el-menu-item>
<el-menu-item index="/admin/contest/create">Create Contest</el-menu-item>
</el-submenu>
</el-menu>
<template slot="title"
><i class="fa fa-bars" aria-hidden="true"></i>Problem</template
>
<el-menu-item index="/admin/problems">Problem List</el-menu-item>
<el-menu-item index="/admin/problem/create"
>Create Problem</el-menu-item
>
<el-menu-item index="/admin/problem/batch-operation"
>Export&Import Problem</el-menu-item
>
</el-submenu>
<el-submenu index="contest">
<template slot="title"
><i class="fa fa-trophy" aria-hidden="true"></i>Contest</template
>
<el-menu-item index="/admin/contest">Contest List</el-menu-item>
<el-menu-item index="/admin/contest/create"
>Create Contest</el-menu-item
>
</el-submenu>
</el-menu>
<div id="header">
<i class="fa fa-font katex-editor" @click="katexVisible=true" ></i>
<el-dropdown @command="handleCommand">
<span>{{user.username}}<i class="el-icon-caret-bottom el-icon--right"></i></span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">Logout</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-row>
<el-col :span="20">
<div class="breadcrumb-container">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/admin/' }"
>Home page</el-breadcrumb-item
>
<el-breadcrumb-item v-for="item in routeList" :key="item.path">
{{ item.name }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</el-col>
<el-col :span="4">
<i class="fa fa-font katex-editor" @click="katexVisible = true"></i>
<el-dropdown @command="handleCommand">
<span
>{{ user.username
}}<i class="el-icon-caret-bottom el-icon--right"></i
></span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">Logout</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
</el-row>
</div>
</div>
<div v-else>
<mu-appbar class="mobile-nav" color="primary">
<mu-appbar class="mobile-nav" color="primary">
<mu-button icon slot="left" @click="opendrawer = !opendrawer">
<i class="el-icon-s-unfold"></i>
</mu-button>
HOJ
HOJ Admin
<mu-menu slot="right" v-show="isAuthenticated">
<mu-button flat @click="katexVisible=true" >
<i class="fa fa-font katex-editor"></i>
<mu-button flat @click="katexVisible = true">
<i class="fa fa-font katex-editor"></i>
</mu-button>
</mu-menu>
<mu-menu slot="right" v-show="isAuthenticated" :open.sync="openusermenu">
<mu-menu
slot="right"
v-show="isAuthenticated"
:open.sync="openusermenu"
>
<mu-button flat>
{{ user.username }}<i class="el-icon-caret-bottom"></i>
</mu-button>
@ -68,75 +104,161 @@
</mu-menu>
</mu-appbar>
<mu-drawer :open.sync="opendrawer" :docked="false" :right="false">
<mu-list toggle-nested>
<mu-list-item button :ripple="true" nested to="/admin/">
<mu-list-item
button
:ripple="true"
nested
to="/admin/"
@click="opendrawer = !opendrawer"
>
<mu-list-item-action>
<mu-icon value="dashboard" size="24"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Dashboard</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" nested :open="openSideMenu === 'general'" @toggle-nested="openSideMenu = arguments[0] ? 'general' : ''">
<mu-list-item
button
:ripple="false"
nested
:open="openSideMenu === 'general'"
@toggle-nested="openSideMenu = arguments[0] ? 'general' : ''"
>
<mu-list-item-action>
<mu-icon value="view_list"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>General</mu-list-item-title>
<mu-list-item-action>
<mu-icon class="toggle-icon" size="24" value="keyboard_arrow_down"></mu-icon>
<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="">
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/user"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>User</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" slot="nested">
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/announcement"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>Announcement</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" slot="nested">
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/conf"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>System Config</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" slot="nested">
<mu-list-item-title>Prune Test Case</mu-list-item-title>
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/delete-test-case"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>Delete Test Case</mu-list-item-title>
</mu-list-item>
</mu-list-item>
<mu-list-item button :ripple="false" nested :open="openSideMenu === 'problem'" @toggle-nested="openSideMenu = arguments[0] ? 'problem' : ''">
<mu-list-item
button
:ripple="false"
nested
:open="openSideMenu === 'problem'"
@toggle-nested="openSideMenu = arguments[0] ? 'problem' : ''"
>
<mu-list-item-action>
<mu-icon value="menu"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Problem</mu-list-item-title>
<mu-list-item-action>
<mu-icon class="toggle-icon" size="24" value="keyboard_arrow_down"></mu-icon>
<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/problems">
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/problems"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>Problem List</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" slot="nested" to="/admin/problem/create">
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/problem/create"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>Create Problem</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" slot="nested" to="/admin/problem/batch_ops">
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/problem/batch-operation"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>Export&Import Problem</mu-list-item-title>
</mu-list-item>
</mu-list-item>
<mu-list-item button :ripple="false" nested :open="openSideMenu === 'contest'" @toggle-nested="openSideMenu = arguments[0] ? 'contest' : ''">
<mu-list-item
button
:ripple="false"
nested
:open="openSideMenu === 'contest'"
@toggle-nested="openSideMenu = arguments[0] ? 'contest' : ''"
>
<mu-list-item-action>
<mu-icon value="assessment"></mu-icon>
</mu-list-item-action>
<mu-list-item-title>Contest</mu-list-item-title>
<mu-list-item-action>
<mu-icon class="toggle-icon" size="24" value="keyboard_arrow_down"></mu-icon>
<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/contest">
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/contest"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>Contest List</mu-list-item-title>
</mu-list-item>
<mu-list-item button :ripple="false" slot="nested" to="/admin/contest/create">
<mu-list-item
button
:ripple="false"
slot="nested"
to="/admin/contest/create"
@click="opendrawer = !opendrawer"
>
<mu-list-item-title>Create Contest</mu-list-item-title>
</mu-list-item>
</mu-list-item>
</mu-list>
</mu-drawer>
</div>
@ -146,169 +268,176 @@
</transition>
</div>
<el-dialog title="Latex Editor" :visible.sync="katexVisible" width="350px" >
<el-dialog title="Latex Editor" :visible.sync="katexVisible" width="350px">
<KatexEditor></KatexEditor>
</el-dialog>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import KatexEditor from '@/components/admin/KatexEditor.vue'
import api from '@/common/api'
import { mapGetters } from "vuex";
import KatexEditor from "@/components/admin/KatexEditor.vue";
import api from "@/common/api";
export default {
name: 'app',
mounted () {
this.currentPath = this.$route.path
window.onresize = () => {
this.page_width();
};
export default {
name: "app",
mounted() {
this.currentPath = this.$route.path;
this.getBreadcrumb();
window.onresize = () => {
this.page_width();
},
data () {
return {
isAuthenticated:true,
openusermenu:false,
openSideMenu:'',
katexVisible: false,
opendrawer:false,
mobileNar: false,
currentPath: '',
user:{
username:"Himit_ZH"
}
};
this.page_width();
},
data() {
return {
isAuthenticated: true,
openusermenu: false,
openSideMenu: "",
katexVisible: false,
opendrawer: false,
mobileNar: false,
currentPath: "",
user: {
username: "Himit_ZH",
},
routeList: [],
};
},
components: {
KatexEditor,
},
methods: {
handleCommand(command) {
if (command === "logout") {
api.logout().then(() => {
this.$router.push({ name: "/admin/login" });
});
}
},
components: {
KatexEditor,
page_width() {
let screenWidth = window.screen.width;
if (screenWidth < 1080) {
this.mobileNar = true;
} else {
this.mobileNar = false;
}
},
methods: {
handleCommand (command) {
if (command === 'logout') {
api.logout().then(() => {
this.$router.push({name: '/admin/login'})
})
}
},
page_width() {
let screenWidth = window.screen.width;
if (screenWidth < 1080) {
this.mobileNar = true;
} else {
this.mobileNar = false;
}
},
getBreadcrumb() {
let matched = this.$route.matched.filter((item) => item.name); //
this.routeList = matched;
},
computed: {
...mapGetters(['userInfo', 'isSuperAdmin', 'hasProblemPermission'])
}
}
},
computed: {
...mapGetters(["userInfo", "isSuperAdmin", "hasProblemPermission"]),
},
watch: {
$route() {
this.getBreadcrumb(); //
},
},
};
</script>
<style scoped>
.vertical_menu {
overflow: auto;
width: 205px;
height: 100%;
position: fixed !important;
z-index: 100;
top: 0;
bottom: 0;
left: 0;
}
.vertical_menu .logo {
margin: 20px 0;
text-align: center;
}
.vertical_menu .logo img {
background-color: #fff;
border-radius: 50%;
border: 3px solid #fff;
width: 75px;
height: 75px;
}
.fa{
margin-right: 5px;
width: 24px;
text-align: center;
font-size: 18px;
}
a {
background-color: transparent;
}
.vertical_menu {
overflow: auto;
width: 205px;
height: 100%;
position: fixed !important;
z-index: 100;
top: 0;
bottom: 0;
left: 0;
}
.vertical_menu .logo {
margin: 20px 0;
text-align: center;
}
.vertical_menu .logo img {
background-color: #fff;
border-radius: 50%;
border: 3px solid #fff;
width: 75px;
height: 75px;
}
.fa {
margin-right: 5px;
width: 24px;
text-align: center;
font-size: 18px;
}
a {
background-color: transparent;
}
a:active, a:hover {
outline-width: 0
}
a:active,
a:hover {
outline-width: 0;
}
img {
border-style: none
}
img {
border-style: none;
}
.admin-container {
overflow: auto;
font-weight: 400;
height: 100%;
-webkit-font-smoothing: antialiased;
background-color: #EDECEC;
overflow-y: auto;
}
.admin-container {
overflow: auto;
font-weight: 400;
height: 100%;
-webkit-font-smoothing: antialiased;
background-color: #edecec;
overflow-y: auto;
}
.breadcrumb-container {
padding: 17px;
background-color: #fff;
}
* {
box-sizing: border-box;
}
* {
box-sizing: border-box;
}
#header {
text-align: right;
padding-left: 210px;
padding-right: 30px;
line-height: 50px;
height: 50px;
background: #f9fafc;
}
#header {
text-align: right;
@media screen and (min-width: 1080px) {
.content-app {
padding-top: 20px;
padding-right: 10px;
padding-left: 210px;
padding-right: 30px;
line-height: 50px;
height: 50px;
background: #F9FAFC;
}
}
@media screen and (max-width: 1080px) {
.content-app {
padding: 0 5px;
margin-top: 20px;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate(0, 30px);
}
#header .screen-full {
margin-right: 8px;
to {
opacity: 1;
transform: none;
}
}
@media screen and (min-width: 1080px) {
.content-app {
padding-top: 20px;
padding-right: 10px;
padding-left: 210px;
}
}
@media screen and (max-width: 1080px) {
.content-app {
padding: 0 5px;
margin-top: 20px;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate(0, 30px);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInUp-enter-active {
animation: fadeInUp .8s;
}
.katex-editor {
margin-right: 5px ;
cursor: pointer;
/*font-size: 18px;*/
}
.fadeInUp-enter-active {
animation: fadeInUp 0.8s;
}
.katex-editor {
margin-right: 5px;
cursor: pointer;
/*font-size: 18px;*/
}
</style>

View File

@ -0,0 +1,227 @@
<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="Contest Title" required>
<el-input v-model="contest.title" placeholder="Enter the Contest Title"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="Contest Description" required>
<Simditor v-model="contest.explain"></Simditor>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest Start Time" required>
<el-date-picker
v-model="contest.startTime"
@change="changeDuration"
type="datetime"
placeholder="Enter the Contest Start Time">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest End Time" required>
<el-date-picker
v-model="contest.endTime"
@change="changeDuration"
type="datetime"
placeholder="Enter the Contest End Time">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest Duration" required>
<el-input v-model="durationText" disabled>
</el-input>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest Type">
<el-radio class="radio" v-model="contest.type" :label="0" :disabled="disableRuleType">ACM</el-radio>
<el-radio class="radio" v-model="contest.type" :label="1" :disabled="disableRuleType">OI</el-radio>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24" v-if="contest.sealRank">
<el-form-item label="Real Time Rank">
<el-switch
v-model="contest.sealRank"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</el-form-item>
</el-col>
<el-col :md="16" :xs="24" v-else>
<el-form-item label="Seal Rank">
<el-switch
v-model="contest.sealRank"
active-color="#13ce66"
inactive-color="">
</el-switch>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Seal Rank Time" :required="contest.sealRank" v-show="contest.sealRank">
<el-select v-model="seal_rank_time" >
<el-option label="比赛结束前半小时" :value="0" :disabled="contest.duration<1800"></el-option>
<el-option label="比赛结束前一小时" :value="1" :disabled="contest.duration<3600"></el-option>
<el-option label="比赛全程时间" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest Auth" required>
<el-select v-model="contest.auth" >
<el-option label="公开赛" :value="0"></el-option>
<el-option label="私有赛" :value="1"></el-option>
<el-option label="保护赛" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Contest Password" v-show="contest.auth!=0" :required="contest.auth!=0">
<el-input v-model="contest.pwd" placeholder="Contest Password"></el-input>
</el-form-item>
</el-col>
<!-- <el-col :span="24">
<el-form-item label="Allowed IP Ranges">
<div v-for="(range, index) in contest.allowed_ip_ranges" :key="index">
<el-row :gutter="20" style="margin-bottom: 15px">
<el-col :span="8">
<el-input v-model="range.value" placeholder="CIDR Network"></el-input>
</el-col>
<el-col :span="10">
<el-button icon="el-icon-plus" @click="addIPRange" type="primary"></el-button>
<el-button icon="el-icon-delete-solid" @click.native="removeIPRange(range)" type="danger"></el-button>
</el-col>
</el-row>
</div>
</el-form-item>
</el-col> -->
</el-row>
</el-form>
<el-button type="primary" @click.native="saveContest">Save</el-button>
</el-card>
</div>
</template>
<script>
import api from '@/common/api'
import Simditor from '@/components/admin/Simditor.vue'
import time from '@/common/time'
import moment from 'moment'
export default {
name: 'CreateContest',
components: {
Simditor
},
data () {
return {
title: 'Create Contest',
disableRuleType: false,
durationText:'', //
seal_rank_time:0, // ,1800s
contest: {
title: '',
explain: '',
startTime: '2020-12-12 12:00:00',
endTime: '2020-12-12 17:00:00',
duration:0,
type: 0,
password: '',
sealRank: true,
sealRankTime:'',//
auth: 0,
allowed_ip_ranges: [{
value: ''
}]
}
}
},
methods: {
saveContest () {
let funcName = this.$route.name === 'edit-contest' ? 'editContest' : 'createContest'
switch(this.seal_rank_time){
case 0: //
this.contest.sealRankTime = moment(this.contest.endTime).subtract(1800,'seconds');
break;
case 1: //
this.contest.sealRankTime = moment(this.contest.endTime).subtract(3600,'seconds');
break;
case 2: //
this.contest.sealRankTime = moment(this.contest.startTime);
}
let data = Object.assign({}, this.contest)
let ranges = []
for (let v of data.allowed_ip_ranges) {
if (v.value !== '') {
ranges.push(v.value)
}
}
data.allowed_ip_ranges = ranges
api[funcName](data).then(res => {
this.$router.push({name: 'admin-contest-list', query: {refresh: 'true'}})
}).catch(() => {
})
},
changeDuration(){
let start = this.contest.startTime;
let end = this.contest.endTime
let durationMS = time.durationMs(start,end);
if(durationMS<0){
this.durationText = '比赛起始时间不应该晚于结束时间!'
this.contest.duration = 0;
return;
}
if(start!=''&&end!=''){
this.durationText = time.duration(start,end);
this.contest.duration = durationMS;
}
},
// addIPRange () {
// this.contest.allowed_ip_ranges.push({value: ''})
// },
// removeIPRange (range) {
// let index = this.contest.allowed_ip_ranges.indexOf(range)
// if (index !== -1) {
// this.contest.allowed_ip_ranges.splice(index, 1)
// }
// }
},
mounted () {
this.changeDuration()
if (this.$route.name === 'admin-edit-contest') {
this.title = 'Edit Contest'
this.disableRuleType = true
api.getContest(this.$route.params.contestId).then(res => {
let data = res.data.data
let ranges = []
for (let v of data.allowed_ip_ranges) {
ranges.push({value: v})
}
if (ranges.length === 0) {
ranges.push({value: ''})
}
data.allowed_ip_ranges = ranges
this.contest = data
}).catch(() => {
})
}
}
}
</script>

View File

@ -0,0 +1,204 @@
<template>
<div>
<el-card>
<div slot="header">
<span class="panel-title home-title">Contest List</span>
<div class="filter-row">
<span>
<vxe-input v-model="keyword" placeholder="Enter keyword" type="search" size="medium" @search-click="filterByKeyword"></vxe-input>
</span>
</div>
</div>
<vxe-table
:loading="loading"
ref="xTable"
:data="contestList"
auto-resize
stripe>
<vxe-table-column
field="id"
min-width="80"
title="ID">
</vxe-table-column>
<vxe-table-column
field="title"
min-width="150"
title="Title">
</vxe-table-column>
<vxe-table-column
title="Type"
min-width="130">
<template v-slot="{row}">
<el-tag type="gray">{{row.type|parseContestType}}</el-tag>
</template>
</vxe-table-column>
<vxe-table-column
title="Auth"
min-width="150">
<template v-slot="{row}">
<el-tooltip :content="CONTEST_TYPE_REVERSE[row.auth].tips" placement="top" effect="light">
<el-tag
:type="CONTEST_TYPE_REVERSE[row.auth].color"
effect="plain">
{{CONTEST_TYPE_REVERSE[row.auth].name}}
</el-tag>
</el-tooltip>
</template>
</vxe-table-column>
<vxe-table-column
title="Status"
min-width="130">
<template v-slot="{row}">
<el-tag
effect="dark"
:color="CONTEST_STATUS_REVERSE[row.status].color"
size="medium"
>
{{CONTEST_STATUS_REVERSE[row.status].name}}
</el-tag>
</template>
</vxe-table-column>
<vxe-table-column
min-width="210"
title="More"
>
<template v-slot="{row}">
<p>Start Time: {{row.startTime | localtime }}</p>
<p>End Time: {{row.endTime | localtime }}</p>
<p>Create Time: {{row.gmtGreate | localtime}}</p>
<p>Creator: {{row.author}}</p>
</template>
</vxe-table-column>
<vxe-table-column
min-width="250"
title="Option">
<template v-slot="{row}">
<el-tooltip effect="dark" content="编辑比赛" 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="查看比赛题目列表" placement="top">
<el-button icon="el-icon-tickets" size="mini" @click.native="goContestProblemList(row.id)" type="success">
</el-button>
</el-tooltip>
<el-tooltip effect="dark" content="查看比赛公告列表" placement="top">
<el-button icon="el-icon-info" size="mini" @click.native="goContestAnnouncement(row.id)" type="info">
</el-button>
</el-tooltip>
<el-tooltip effect="dark" content="下载通过的提交列表" placement="top">
<el-button icon="el-icon-download" size="mini" @click.native="openDownloadOptions(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>
<el-dialog title="Download Contest Submissions"
width="350px"
:visible.sync="downloadDialogVisible">
<el-switch v-model="excludeAdmin" active-text="Exclude admin submissions"></el-switch>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="downloadSubmissions"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import api from '@/common/api'
import utils from '@/common/utils'
import {CONTEST_STATUS_REVERSE,CONTEST_TYPE_REVERSE} from '@/common/constants'
export default {
name: 'ContestList',
data () {
return {
pageSize: 10,
total: 0,
contestList: [
{id:1000,startTime:'2020-12-12 12:00:00',endTime:'2020-12-12 17:00:00',gmtGreate:'2020-12-11 12:00:00',
author:'Himit_ZH',title:'测试比赛',type:0,auth:0,status:-1
}
],
keyword: '',
loading: false,
excludeAdmin: true,
currentPage: 1,
currentId: 1,
downloadDialogVisible: false,
CONTEST_TYPE_REVERSE:{},
}
},
mounted () {
this.CONTEST_TYPE_REVERSE = Object.assign({},CONTEST_TYPE_REVERSE)
this.CONTEST_STATUS_REVERSE = Object.assign({},CONTEST_STATUS_REVERSE)
// this.getContestList(this.currentPage)
},
methods: {
//
currentChange (page) {
this.currentPage = page
this.getContestList(page)
},
getContestList (page) {
this.loading = true
api.getContestList((page - 1) * this.pageSize, this.pageSize, this.keyword).then(res => {
this.loading = false
this.total = res.data.data.total
this.contestList = res.data.data.results
}, res => {
this.loading = false
})
},
openDownloadOptions (contestId) {
this.downloadDialogVisible = true
this.currentId = contestId
},
downloadSubmissions () {
let excludeAdmin = this.excludeAdmin ? '1' : '0'
let url = `/admin/download_submissions?contest_id=${this.currentId}&exclude_admin=${excludeAdmin}`
utils.downloadFile(url)
},
goEdit (contestId) {
this.$router.push({name: 'admin-edit-contest', params: {contestId}})
},
goContestAnnouncement (contestId) {
this.$router.push({name: 'admin-contest-announcement', params: {contestId}})
},
goContestProblemList (contestId) {
this.$router.push({name: 'admin-contest-problem-list', params: {contestId}})
},
filterByKeyword(){
this.currentChange(1)
}
},
}
</script>
<style scoped>
.filter-row{
margin-top: 10px;
}
@media screen and (max-width: 768px) {
.filter-row span {
margin-right: 5px;
}
}
@media screen and (min-width: 768px) {
.filter-row span {
margin-right: 20px;
}
}
.el-tag--dark{
border-color: #FFF;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div>
<el-card>
<span slot="header" class="panel-title home-title">Prune Test Case
<span slot="header" class="panel-title home-title">Delete Test Case
<el-popover placement="right" trigger="hover">
这些测试用例不属于任何问题您可以安全地清理它们
<i slot="reference" class="el-icon-question"></i>

View File

@ -44,11 +44,11 @@
</vxe-table-column>
<vxe-table-column title="Option" min-width="150">
<template v-slot="{ row }">
<el-tooltip class="item" effect="dark" content="编辑用户" placement="top">
<el-tooltip effect="dark" content="编辑用户" placement="top">
<el-button icon="el-icon-edit-outline" size="mini" @click.native="openUserDialog(row)" type="primary">
</el-button>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="删除用户" placement="top">
<el-tooltip effect="dark" content="删除用户" placement="top">
<el-button icon="el-icon-delete-solid" size="mini" @click.native="deleteUsers([row.uid])" type="danger">
</el-button>
</el-tooltip>
@ -78,7 +78,7 @@
:show-file-list="false"
accept=".csv"
:before-upload="handleUsersCSV">
<el-button size="small" icon="el-icon-upload" type="primary">Choose File</el-button>
<el-button size="small" icon="el-icon-folder-opened" type="primary">Choose File</el-button>
</el-upload>
<template v-else>
@ -319,7 +319,7 @@
})
},
filterByKeyword(){
console.log("ssssssss")
this.currentChange(1)
},
//
openUserDialog (row) {
@ -418,9 +418,6 @@
}
},
watch: {
'keyword' () {
this.currentChange(1)
},
'uploadUsersCurrentPage' (page) {
this.uploadUsersPage = this.uploadUsers.slice((page - 1) * this.uploadUsersPageSize, page * this.uploadUsersPageSize)
}

View File

@ -0,0 +1,182 @@
<template>
<div>
<el-card>
<div slot="header">
<span class="panel-title home-title">Export Problems</span>
<div class="filter-row">
<span>
<el-button type="primary" size="small"
@click="exportProblems" icon="el-icon-arrow-down">Export
</el-button>
</span>
<span>
<vxe-input v-model="keyword" placeholder="Enter keyword" type="search" size="medium" @search-click="filterByKeyword"></vxe-input>
</span>
</div>
</div>
<vxe-table :data="problems" stripe auto-resize
ref="xTable"
:loading="loadingProblems"
:checkbox-config="{labelField: '', highlight: true, range: true}"
@checkbox-change="handleSelectionChange"
@checkbox-all="handlechangeAll">
<vxe-table-column type="checkbox" width="60">
</vxe-table-column>
<vxe-table-column
title="ID"
min-width="100"
field="id">
</vxe-table-column>
<vxe-table-column
min-width="150"
title="Title"
field="title">
</vxe-table-column>
<vxe-table-column
min-width="150"
field="author"
title="Author">
</vxe-table-column>
<vxe-table-column
field="gmtCreate"
title="Create Time">
<template v-slot="{row}">
{{row.create_time | localtime }}
</template>
</vxe-table-column>
</vxe-table>
<div class="panel-options">
<el-pagination
class="page"
layout="prev, pager, next"
@current-change="getProblems"
:page-size="limit"
:total="total">
</el-pagination>
</div>
</el-card>
<el-card style="margin-top:15px">
<div slot="header">
<span class="panel-title home-title">Import QDUOJ Problems</span>
</div>
<el-upload
ref="QDU"
action="/api/admin/import_problem"
name="file"
:file-list="fileList1"
:show-file-list="true"
:with-credentials="true"
:limit="3"
:on-change="onFile1Change"
:auto-upload="false"
:on-success="uploadSucceeded"
:on-error="uploadFailed">
<el-button size="small" type="primary" slot="trigger" icon="el-icon-folder-opened">Choose File</el-button>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload('QDU')" icon="el-icon-upload">Upload</el-button>
</el-upload>
</el-card>
</div>
</template>
<script>
import api from '@/common/api'
import utils from '@/common/utils'
export default {
name: 'import_and_export',
data () {
return {
fileList1: [],
page: 1,
limit: 10,
total: 0,
loadingProblems: false,
loadingImporting: false,
keyword: '',
problems: [
{id:1001,author:'Himit_ZH',title:'测试题目',gmtCreate:'2020-11-11 11:11:11'}
],
selected_problems: []
}
},
mounted () {
// this.getProblems()
},
methods: {
//
handleSelectionChange ({records }) {
this.selected_problems = records
},
//
handlechangeAll () {
this.selected_problems = this.$refs.xTable.getCheckboxRecords();
},
getProblems (page = 1) {
let params = {
keyword: this.keyword,
offset: (page - 1) * this.limit,
limit: this.limit
}
this.loadingProblems = true
api.getProblemList(params).then(res => {
this.problems = res.data.data.results
this.total = res.data.data.total
this.loadingProblems = false
})
},
exportProblems () {
let params = []
for (let p of this.selected_problems) {
params.push('problem_id=' + p.id)
}
let url = '/admin/export_problem?' + params.join('&')
utils.downloadFile(url)
},
submitUpload (ref) {
this.$refs[ref].submit()
},
onFile1Change (file, fileList) {
this.fileList1 = fileList.slice(-1)
},
onFile2Change (file, fileList) {
this.fileList2 = fileList.slice(-1)
},
uploadSucceeded (response) {
if (response.error) {
this.$error(response.data)
} else {
this.$success('Successfully imported ' + response.data.import_count + ' problems')
this.getProblems()
}
},
uploadFailed () {
this.$error('Upload failed')
},
filterByKeyword(){
this.getProblems()
},
}
}
</script>
<style scoped>
.filter-row{
margin-top: 10px;
}
@media screen and (max-width: 768px) {
.filter-row span {
margin-right: 5px;
}
}
@media screen and (min-width: 768px) {
.filter-row span {
margin-right: 20px;
}
}
</style>

View File

@ -0,0 +1,917 @@
<template>
<div class="problem">
<el-card>
<div slot="header">
<span class="panel-title home-title">{{ title }}</span>
</div>
<el-form
ref="form"
:model="problem"
:rules="rules"
label-position="top"
label-width="70px"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item prop="title" label="Title" required>
<el-input
placeholder="Enter the title of problem"
v-model="problem.title"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item prop="description" label="Description" required>
<Simditor v-model="problem.description"></Simditor>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :md="8" :xs="24">
<el-form-item label="Time Limit(ms)" required>
<el-input
type="Number"
placeholder="Enter the time limit of problem"
v-model="problem.timeLimit"
></el-input>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Memory Limit(mb)" required>
<el-input
type="Number"
placeholder="Enter the memory limit of problem"
v-model="problem.memoryLimit"
></el-input>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item label="Level" required>
<el-select
class="difficulty-select"
placeholder="Enter the level of problem"
v-model="problem.difficulty"
>
<el-option label="Easy" :value="0"></el-option>
<el-option label="Mid" :value="1"></el-option>
<el-option label="Hard" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item
prop="input_description"
label="Input Description"
required
>
<Simditor v-model="problem.input"></Simditor>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item
prop="output_description"
label="Output Description"
required
>
<Simditor v-model="problem.output"></Simditor>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :md="4" :xs="24">
<el-form-item label="Auth">
<el-select v-model="problem.auth" size="small">
<el-option label="公开" :value="1"></el-option>
<el-option label="私有" :value="2"></el-option>
<el-option label="比赛中" :value="3"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :md="4" :xs="24">
<el-form-item label="Code Shareable">
<el-switch
v-model="problem.code_share"
active-text=""
inactive-text="">
</el-switch>
</el-form-item>
</el-col>
<el-col :md="16" :xs="24">
<el-form-item label="Tags" required>
<el-tag
v-for="tag in problem.tags"
closable
:close-transition="false"
:key="tag.name"
size="small"
@close="closeTag(tag.name)"
style="margin-right: 7px;margin-top:4px"
>{{ tag.name }}</el-tag
>
<!-- 输入时建议回车选择光标消失触发更新 -->
<el-autocomplete
v-if="inputVisible"
size="mini"
class="input-new-tag"
v-model="tagInput"
:trigger-on-focus="false"
@keyup.enter.native="addTag"
@blur="addTag"
@select="addTag"
:fetch-suggestions="querySearch"
>
</el-autocomplete>
<el-tooltip
effect="dark"
content="添加新标签"
placement="top"
v-else
>
<el-button
class="button-new-tag"
size="small"
@click="inputVisible = true"
icon="el-icon-plus"
></el-button>
</el-tooltip>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :md="24" :xs="24">
<el-form-item label="Language" :error="error.languages" required>
<el-checkbox-group v-model="problem.languages">
<el-tooltip
class="spj-radio"
v-for="lang in allLanguage.languages"
:key="lang.name"
effect="dark"
:content="lang.description"
placement="top-start"
>
<el-checkbox :label="lang.name"></el-checkbox>
</el-tooltip>
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<div>
<div class="panel-title home-title">
Problem Examples
<el-popover placement="right" trigger="hover">
<p>题目样例请最好不要超过2个题目样例题面样例不纳入评测数据</p>
<i slot="reference" class="el-icon-question"></i>
</el-popover>
</div>
<el-form-item
v-for="(example, index) in problem.examples"
:key="'example' + index"
>
<Accordion :title="'Example' + (index + 1)">
<el-button
type="danger"
size="small"
icon="el-icon-delete"
slot="header"
@click="deleteExample(index)"
>
Delete
</el-button>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="Input Example" required>
<el-input
:rows="5"
type="textarea"
placeholder="Input Example"
v-model="example.input"
>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Output Example" required>
<el-input
:rows="5"
type="textarea"
placeholder="Output Example"
v-model="example.output"
>
</el-input>
</el-form-item>
</el-col>
</el-row>
</Accordion>
</el-form-item>
</div>
<div class="add-example-btn">
<el-button
class="add-examples"
@click="addExample()"
icon="el-icon-plus"
type="small"
>Add Example
</el-button>
</div>
<div>
<div class="panel-title home-title">
Judge Samples
<el-popover placement="right" trigger="hover">
<p>评测数据判题机对该题目的相关提交进行评测的数据来源</p>
<i slot="reference" class="el-icon-question"></i>
</el-popover>
</div>
<el-form-item
v-for="(sample, index) in problem.samples"
:key="'sample' + index"
>
<Accordion :title="'Sample' + (index + 1)">
<el-button
type="danger"
size="small"
icon="el-icon-delete"
slot="header"
@click="deleteSample(index)"
>
Delete
</el-button>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="Input Sample" required>
<el-input
:rows="5"
type="textarea"
placeholder="Input Sample"
v-model="sample.input"
>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Output Sample" required>
<el-input
:rows="5"
type="textarea"
placeholder="Output Sample"
v-model="sample.output"
>
</el-input>
</el-form-item>
</el-col>
</el-row>
</Accordion>
</el-form-item>
</div>
<div class="add-sample-btn">
<el-button
class="add-samples"
@click="addSample()"
icon="el-icon-plus"
type="small"
>Add Sample
</el-button>
</div>
<div class="panel-title home-title">
Special Judge
<el-popover placement="right" trigger="hover">
<p>使用特殊判题的原因</p>
<p>1. 题目要求的输出结果可能不唯一允许不同结果存在</p>
<p>2. 题目最终要求输出一个浮点数而且会告诉只要答案和标准答案相差不超过某个较小的数就可以
例如题目要求保留几位小数输出结果后几位小数不相同也是正确的</p>
<i slot="reference" class="el-icon-question"></i>
</el-popover>
</div>
<el-form-item label="" :error="error.spj">
<el-col :span="24">
<el-checkbox
v-model="problem.spj"
@click.native.prevent="switchSpj()"
>Use Special Judge</el-checkbox
>
</el-col>
</el-form-item>
<el-form-item v-if="problem.spj">
<Accordion title="Special Judge Code">
<template slot="header">
<span style="margin-right:5px;">SPJ language</span>
<el-radio-group v-model="problem.spj_language">
<el-tooltip
class="spj-radio"
v-for="lang in allLanguage.spj_languages"
:key="lang.name"
effect="dark"
:content="lang.description"
placement="top-start"
>
<el-radio :label="lang.name">{{ lang.name }}</el-radio>
</el-tooltip>
</el-radio-group>
<el-button
type="primary"
size="small"
icon="el-icon-fa-random"
@click="compileSPJ"
:loading="loadingCompile"
style="margin-left:10px"
>Complie
</el-button>
</template>
<code-mirror
v-model="problem.spj_code"
:mode="spjMode"
></code-mirror>
</Accordion>
</el-form-item>
<el-form-item style="margin-top: 20px" label="Hint">
<Simditor v-model="problem.hint"></Simditor>
</el-form-item>
<el-row :gutter="20">
<el-col :span="4">
<el-form-item label="Type">
<el-radio-group
v-model="problem.type"
:disabled="disableRuleType"
>
<el-radio :label="0" >ACM</el-radio>
<el-radio :label="1" >OI</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="TestCase" :error="error.testcase">
<el-upload
action="/api/admin/test_case"
name="file"
:data="{ spj: problem.spj }"
:show-file-list="true"
:on-success="uploadSucceeded"
:on-error="uploadFailed"
>
<el-button size="small" type="primary" icon="el-icon-upload"
>Choose File</el-button
>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="24">
<vxe-table stripe auto-resize :data="problem.test_case_score">
<vxe-table-column field="input_name" title="Input" min-width="150">
</vxe-table-column>
<vxe-table-column field="output_name" title="Output" min-width="150">
</vxe-table-column>
<vxe-table-column field="score" title="Score" min-width="150">
<template v-slot="{ row }">
<el-input
size="small"
placeholder="Score"
v-model="row.score"
:disabled="problem.type !== 'OI'"
>
</el-input>
</template>
</vxe-table-column>
</vxe-table>
</el-col>
</el-row>
<el-form-item label="Source">
<el-input
placeholder="Enter the problem where from"
v-model="problem.source"
></el-input>
</el-form-item>
<el-button type="primary" @click.native="submit()" size="small"
>Save</el-button
>
</el-form>
</el-card>
</div>
</template>
<script>
import Simditor from "@/components/admin/Simditor";
import Accordion from "@/components/admin/Accordion";
import CodeMirror from "@/components/admin/CodeMirror";
import api from "@/common/api";
export default {
name: "Problem",
components: {
Simditor,
Accordion,
CodeMirror,
},
data() {
return {
rules: {
title: {
required: true,
message: "Title is required",
trigger: "blur",
},
input_description: {
required: true,
message: "Input Description is required",
trigger: "blur",
},
output_description: {
required: true,
message: "Output Description is required",
trigger: "blur",
},
},
loadingCompile: false,
mode: "", //
contest: {},
problem: {
id: "",
title: "",
description: "",
input_description: "",
output_description: "",
time_limit: 1000,
memory_limit: 256,
difficulty: 0,
auth: 1,
code_share: true,
tags: [
{
id:1001,
name:'模拟题'
},
{
id:1002,
name:'递归题'
}
],
languages: [],
examples:[{ input: "", output: "" }], //
samples: [{ input: "", output: "" }], // 使
spj: false,
spj_language: "",
spj_code: "",
spj_compile_ok: false,
test_case_id: "",
test_case_score: [
{input_name:'1.in',output_name:'1.out',score:100}
],
type: 0,
hint: "",
source: "",
},
reProblem: {
languages: [],
},
testCaseUploaded: false,
allLanguage: {
languages:[
{
content_type: "text/x-csrc",
description: "GCC 5.4",
name: "C",
compile_command: "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c11 {src_path} -lm -o {exe_path}",
template: "#include <stdio.h>\nint add(int a, int b) {\n return a+b;\n}\nint main() {\n printf(\"%d\", add(1, 2));\n return 0;\n}"
},
{
content_type: "text/x-c++src",
description: "G++ 5.4",
name: "C++",
compile_command: "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++14 {src_path} -lm -o {exe_path}",
template: "#include <iostream>\nint add(int a, int b) {\n return a+b;\n}\nint main() {\n std::cout << add(1, 2);\n return 0;\n}",
},
{ content_type: "text/x-java",
description: "OpenJDK 1.8",
name: "Java",
compile_command: "/usr/bin/javac {src_path} -d {exe_dir} -encoding UTF8",
template: "import java.util.Scanner;\npublic class Main{\n public static void main(String[] args){\n Scanner in=new Scanner(System.in);\n int a=in.nextInt();\n int b=in.nextInt();\n System.out.println((a+b));\n }\n}"
},
{
content_type: "text/x-python",
description: "Python 3.7",
name: "Python3",
template: "a, b = map(int, input().split())\nprint(a + b)",
compile_command: "/usr/bin/python3 -m py_compile {src_path}"
}
],
spj_languages:[
{
content_type: "text/x-csrc",
description: "GCC 5.4",
name: "C",
compile_command: "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c11 {src_path} -lm -o {exe_path}",
template: "#include <stdio.h>\nint add(int a, int b) {\n return a+b;\n}\nint main() {\n printf(\"%d\", add(1, 2));\n return 0;\n}"
},
{
content_type: "text/x-c++src",
description: "G++ 5.4",
name: "C++",
compile_command: "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++14 {src_path} -lm -o {exe_path}",
template: "#include <iostream>\nint add(int a, int b) {\n return a+b;\n}\nint main() {\n std::cout << add(1, 2);\n return 0;\n}",
},
]
},
inputVisible: false,
tagInput: '',
title: "",
spjMode: "",
disableRuleType: false,
routeName: "",
error: {
tags: "",
spj: "",
languages: "",
testCase: "",
},
};
},
mounted() {
this.routeName = this.$route.name;
if (
this.routeName === "edit-problem" ||
this.routeName === "edit-contest-problem"
) {
this.mode = "edit";
} else {
this.mode = "add";
}
api.getLanguages().then((res) => {
this.problem = this.reProblem = {
id: "",
title: "",
description: "",
input_description: "",
output_description: "",
time_limit: 1000,
memory_limit: 256,
difficulty: 0,
visible: true,
share_submission: false,
tags: [],
languages: [],
samples: [{ input: "", output: "" }],
spj: false,
spj_language: "",
spj_code: "",
spj_compile_ok: false,
test_case_id: "",
test_case_score: [],
type: 0,
hint: "",
source: "",
};
let contestID = this.$route.params.contestId;
if (contestID) {
this.problem.contest_id = this.reProblem.contest_id = contestID;
this.disableRuleType = true;
api.getContest(contestID).then((res) => {
this.problem.type = this.reProblem.type =
res.data.data.type;
this.contest = res.data.data;
});
}
this.problem.spj_language = "C";
let allLanguage = res.data.data;
this.allLanguage = allLanguage;
// get problem after getting languages list to avoid find undefined value in `watch problem.languages`
if (this.mode === "edit") {
this.title = "Edit Problem";
let funcName = {
"admin-edit-problem": "getProblem",
"admin-edit-contest-problem": "getContestProblem",
}[this.routeName];
// api[funcName](this.$route.params.problemId).then((problemRes) => {
// let data = problemRes.data.data;
// if (!data.spj_code) {
// data.spj_code = "";
// }
// data.spj_language = data.spj_language || "C";
// this.problem = data;
// this.testCaseUploaded = true;
// });
} else {
this.title = "Add Problem";
for (let item of allLanguage.languages) {
this.problem.languages.push(item.name);
}
}
});
},
watch: {
$route() {
this.$refs.form.resetFields();
this.problem = this.reProblem;
},
"problem.spj_language"(newVal) {
this.spjMode = this.allLanguage.spj_languages.find((item) => {
return item.name === this.problem.spj_language;
}).content_type;
},
},
methods: {
switchSpj() {
if (this.testCaseUploaded) {
this.$confirm(
"If you change problem judge method, you need to re-upload test cases",
"Warning",
{
confirmButtonText: "Yes",
cancelButtonText: "Cancel",
type: "warning",
}
)
.then(() => {
this.problem.spj = !this.problem.spj;
this.resetTestCase();
})
.catch(() => {});
} else {
this.problem.spj = !this.problem.spj;
}
},
querySearch(queryString, callback) {
// api
// .getProblemTagList()
// .then((res) => {
// let tagList = [];
// for (let tag of res.data.data) {
// tagList.push({ value: tag.name});
// }
// callback(tagList);
// })
// .catch(() => {});
let tagList = [{value:'简单题'}];
callback(tagList);
},
resetTestCase() {
this.testCaseUploaded = false;
this.problem.test_case_score = [];
this.problem.test_case_id = "";
},
addTag() {
let newTag ={
name:this.tagInput,
}
if (newTag) {
this.problem.tags.push(newTag);
}
this.inputVisible = false;
this.tagInput = '';
},
// tagIDtags
closeTag(tag) {
this.problem.tags.splice(this.problem.tags.indexOf(tag), 1);
},
//
addExample(){
this.problem.examples.push({ input: "", output: "" });
},
//
addSample() {
this.problem.samples.push({ input: "", output: "" });
},
//
deleteExample(index){
this.problem.examples.splice(index, 1);
},
//
deleteSample(index) {
this.problem.samples.splice(index, 1);
},
uploadSucceeded(response) {
if (response.error) {
this.$error(response.data);
return;
}
let fileList = response.data.info;
for (let file of fileList) {
file.score = (100 / fileList.length).toFixed(0);
if (!file.output_name && this.problem.spj) {
file.output_name = "-";
}
}
this.problem.test_case_score = fileList;
this.testCaseUploaded = true;
this.problem.test_case_id = response.data.id;
},
uploadFailed() {
this.$error("Upload failed");
},
compileSPJ() {
let data = {
id: this.problem.id,
spj_code: this.problem.spj_code,
spj_language: this.problem.spj_language,
};
this.loadingCompile = true;
api.compileSPJ(data).then(
(res) => {
this.loadingCompile = false;
this.problem.spj_compile_ok = true;
this.error.spj = "";
},
(err) => {
this.loadingCompile = false;
this.problem.spj_compile_ok = false;
const h = this.$createElement;
this.$msgbox({
title: "Compile Error",
type: "error",
message: h("pre", err.data.data),
showCancelButton: false,
closeOnClickModal: false,
customClass: "dialog-compile-error",
});
}
);
},
submit() {
if (!this.problem.samples.length) {
this.$error("Sample is required");
return;
}
for (let sample of this.problem.samples) {
if (!sample.input || !sample.output) {
this.$error("Sample input and output is required");
return;
}
}
if (!this.problem.tags.length) {
this.error.tags = "Please add at least one tag";
this.$error(this.error.tags);
return;
}
if (this.problem.spj) {
if (!this.problem.spj_code) {
this.error.spj = "Spj code is required";
this.$error(this.error.spj);
} else if (!this.problem.spj_compile_ok) {
this.error.spj = "SPJ code has not been successfully compiled";
}
if (this.error.spj) {
this.$error(this.error.spj);
return;
}
}
if (!this.problem.languages.length) {
this.error.languages =
"Please choose at least one language for problem";
this.$error(this.error.languages);
return;
}
if (!this.testCaseUploaded) {
this.error.testCase = "Test case is not uploaded yet";
this.$error(this.error.testCase);
return;
}
if (this.problem.type === "OI") {
for (let item of this.problem.test_case_score) {
try {
if (parseInt(item.score) <= 0) {
this.$error("Invalid test case score");
return;
}
} catch (e) {
this.$error("Test case score must be an integer");
return;
}
}
}
this.problem.languages = this.problem.languages.sort();
let funcName = {
"create-problem": "createProblem",
"edit-problem": "editProblem",
"create-contest-problem": "createContestProblem",
"edit-contest-problem": "editContestProblem",
}[this.routeName];
// edit contest problem , contest_id
if (funcName === "editContestProblem") {
this.problem.contest_id = this.contest.id;
}
api[funcName](this.problem)
.then((res) => {
if (
this.routeName === "create-contest-problem" ||
this.routeName === "edit-contest-problem"
) {
this.$router.push({
name: "contest-problem-list",
params: { contestId: this.$route.params.contestId },
});
} else {
this.$router.push({ name: "problem-list" });
}
})
.catch(() => {});
},
},
};
</script>
<style scoped>
/deep/.el-form-item__label {
padding: 0!important;
}
.el-form-item {
margin-bottom: 10px!important;
}
.difficulty-select {
width: 120px;
}
.input-new-tag {
width: 78px;
}
.button-new-tag {
height: 24px;
line-height: 22px;
padding-top: 0;
padding-bottom: 0;
}
.accordion {
margin-bottom: 10px;
}
.add-examples {
width: 100%;
background-color: #fff;
border: 1px dashed #2d8cf0;
outline: none;
cursor: pointer;
color: #2d8cf0;
height: 35px;
font-size: 14px;
}
.add-examples i {
margin-right: 10px;
}
.add-examples:hover {
border:0px;
background-color: #2d8cf0;
color: #fff;
}
.add-example-btn {
margin-bottom: 10px;
}
.add-samples {
width: 100%;
background-color: #fff;
border: 1px dashed #19be6b;
outline: none;
cursor: pointer;
color: #19be6b;
height: 35px;
font-size: 14px;
}
.add-samples i {
margin-right: 10px;
}
.add-samples:hover {
border:0px;
background-color: #19be6b;
color: #fff;
}
.add-sample-btn {
margin-bottom: 10px;
}
.dialog-compile-error {
width: auto;
max-width: 80%;
overflow-x: scroll;
}
</style>

View File

@ -0,0 +1,245 @@
<template>
<div>
<el-card>
<div slot="header">
<span class="panel-title home-title">{{contestId ?'Contest Problem List': 'Problem List'}}</span>
<div class="filter-row">
<span>
<el-button type="primary" size="small"
@click="goCreateProblem" icon="el-icon-plus">Create
</el-button>
<el-button v-if="contestId" type="primary"
size="small" icon="el-icon-plus"
@click="addProblemDialogVisible = true">Add From Public Problem
</el-button>
</span>
<span>
<vxe-input v-model="keyword" placeholder="Enter keyword" type="search" size="medium" @search-click="filterByKeyword"></vxe-input>
</span>
</div>
</div>
<vxe-table stripe auto-resize :data="problemList"
ref="xTable"
:loading="loading"
@row-dblclick="handleDblclick"
align="center"
>
<vxe-table-column
min-width="100"
field="id"
title="ID">
</vxe-table-column>
<vxe-table-column
field="title"
min-width="150"
title="Title">
</vxe-table-column>
<vxe-table-column
field="author"
min-width="150"
title="Author">
</vxe-table-column>
<vxe-table-column
min-width="150"
field="gmtCreate"
title="Create Time">
<template v-slot="{row}">
{{row.create_time | localtime }}
</template>
</vxe-table-column>
<vxe-table-column
min-width="100"
field="auth"
title="Auth">
<template v-slot="{row}">
<el-select v-model="row.auth" @change="updateProblem(row)" size="small">
<el-option label="公开" :value="1"></el-option>
<el-option label="私有" :value="2"></el-option>
<el-option label="比赛中" :value="3"></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="编辑题目" placement="top">
<el-button icon="el-icon-edit-outline" size="mini" @click.native="goEdit(row)" type="primary">
</el-button>
</el-tooltip>
<el-tooltip effect="dark" content="下载测试样例" placement="top">
<el-button icon="el-icon-download" size="mini" @click.native="downloadTestCase(row.id)" type="success">
</el-button>
</el-tooltip>
<el-tooltip effect="dark" content="删除题目" placement="top">
<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"
@current-change="currentChange"
:page-size="pageSize"
:total="total">
</el-pagination>
</div>
</el-card>
<el-dialog title="Add Contest Problem"
v-if="contestId"
width="90%"
:visible.sync="addProblemDialogVisible"
@close-on-click-modal="false">
<ContestAddProblem :contestID="contestId" @on-change="getProblemList"></ContestAddProblem>
</el-dialog>
</div>
</template>
<script>
import api from '@/common/api'
import utils from '@/common/utils'
import ContestAddProblem from '@/components/admin/ContestAddProblem.vue'
export default {
name: 'ProblemList',
components: {
ContestAddProblem
},
data () {
return {
pageSize: 10,
total: 0,
problemList: [
{id:1001,title:'测试标题',author:'Himit_ZH',gmtCreate:'2020-11-11 22:22:22',auth:1}
],
keyword: '',
loading: false,
currentPage: 1,
routeName: '',
contestId: '',
// for make public use
currentProblemID: '',
currentRow: {},
addProblemDialogVisible: false
}
},
mounted () {
this.routeName = this.$route.name
this.contestId = this.$route.params.contestId
// this.getProblemList(this.currentPage)
},
methods: {
handleDblclick (row) {
row.isEditing = true
},
goEdit (problemId) {
if (this.routeName === 'admin-problem-list') {
this.$router.push({name: 'admin-edit-problem', params: {problemId}})
} else if (this.routeName === 'admin-contest-problem-list') {
this.$router.push({name: 'admin-edit-contest-problem', params: {problemId: problemId, contestId: this.contestId}})
}
},
goCreateProblem () {
if (this.routeName === 'admin-problem-list') {
this.$router.push({name: 'admin-create-problem'})
} else if (this.routeName === 'admin-contest-problem-list') {
this.$router.push({name: 'admin-create-contest-problem', params: {contestId: this.contestId}})
}
},
//
currentChange (page) {
this.currentPage = page
this.getProblemList(page)
},
getProblemList (page = 1) {
this.loading = true
let funcName = this.routeName === 'admin-problem-list' ? 'getProblemList' : 'getContestProblemList'
let params = {
limit: this.pageSize,
offset: (page - 1) * this.pageSize,
keyword: this.keyword,
contest_id: this.contestId
}
api[funcName](params).then(res => {
this.loading = false
this.total = res.data.data.total
for (let problem of res.data.data.results) {
problem.isEditing = false
}
this.problemList = res.data.data.results
}, err => {
this.loading = false
})
},
deleteProblem (id) {
this.$confirm('确定要删除此问题吗?注意:该问题的相关提交数据也将被删除。', '删除题目', {
type: 'warning'
}).then(() => {
let funcName = this.routeName === 'admin-problem-list' ? 'deleteProblem' : 'deleteContestProblem'
api[funcName](id).then(() => [
this.getProblemList(this.currentPage - 1)
]).catch(() => {
})
}, () => {
})
},
updateProblem (row) {
let data = Object.assign({}, row)
let funcName = ''
if (this.contestId) {
data.contest_id = this.contestId
funcName = 'editContestProblem'
} else {
funcName = 'editProblem'
}
api[funcName](data).then(res => {
this.getProblemList(this.currentPage)
}).catch(() => {
})
},
downloadTestCase (problemID) {
let url = '/admin/test_case?problem_id=' + problemID
utils.downloadFile(url)
},
getPublicProblem () {
api.getProblemList()
},
filterByKeyword(){
this.currentChange()
}
},
watch: {
'$route' (newVal, oldVal) {
this.contestId = newVal.params.contestId
this.routeName = newVal.name
this.getProblemList(this.currentPage)
}
}
}
</script>
<style scoped>
.filter-row{
margin-top: 10px;
}
@media screen and (max-width: 768px) {
.filter-row span {
margin-right: 5px;
}
}
@media screen and (min-width: 768px) {
.filter-row span {
margin-right: 20px;
}
}
</style>

View File

@ -64,7 +64,7 @@
</vxe-table-column>
<vxe-table-column min-width="80" v-for="problem in problems" :key="problem.id">
<template v-slot:header>
<span><a @click="getContestProblemById(problem.id)">{{problem.id}}</a></span>
<span><a @click="getContestProblemById(problem.id)" style="color:#495060;">{{problem.id}}</a></span>
</template>
<template v-slot="{ row }">
<span v-if="row.submission_info[problem.id].is_ac">{{ row.submission_info[problem.id].ac_time }}<br></span>

View File

@ -4,6 +4,7 @@
border="inner"
stripe
auto-resize
highlight-hover-row
:data="problems"
@cell-click="goContestProblem">
<vxe-table-column field="status" title="" width="50">

View File

@ -477,9 +477,9 @@
padding: 8px!important;
}
a.emphasis{
color:#495060
color:#495060!important;
}
a.emphasis:hover{
color:#2d8cf0
color:#2d8cf0;
}
</style>

View File

@ -251,7 +251,6 @@ export default {
},
],
tags: ['简单题','模拟题'],
io_mode: { io_mode: "Standard IO" },
},
pie: pie,
largePie: largePie,

View File

@ -53,9 +53,9 @@
</template>
</vxe-table-column>
<vxe-table-column field="level" title="Level" min-width="100">
<vxe-table-column field="difficulty" title="Level" min-width="100">
<template v-slot="{ row }">
<span :class="getLevelColor(row.level)">{{PROBLEM_LEVEL[row.level].name}}</span>
<span :class="getLevelColor(row.difficulty)">{{PROBLEM_LEVEL[row.difficulty].name}}</span>
</template>
</vxe-table-column>
@ -136,15 +136,15 @@
{status:5,count:70},
],
problemList: [
{myStatus:-10,pid:'1000',title:'测试标题',level:0,
{myStatus:-10,pid:'1000',title:'测试标题',difficulty:0,
tags:['简单题','模拟题'],
total:'10000',ACRate:'59.12%'
},
{myStatus:-1,pid:'1000',title:'测试标题',level:1,
{myStatus:-1,pid:'1000',title:'测试标题',difficulty:1,
tags:['简单题','模拟题','递归题','递归题'],
total:'10000',ACRate:'59.12%'
},
{myStatus:0,pid:'1000',title:'测试标题',level:2,
{myStatus:0,pid:'1000',title:'测试标题',difficulty:2,
tags:['简单题','模拟题'],
total:'10000',ACRate:'59.12%'
},
@ -246,8 +246,8 @@
getProblemUri(pid){
return '/problem/'+pid;
},
getLevelColor(level){
return 'el-tag el-tag--small status-'+PROBLEM_LEVEL[level].color;
getLevelColor(difficulty){
return 'el-tag el-tag--small status-'+PROBLEM_LEVEL[difficulty].color;
},
getIconColor(status){
console.log(JUDGE_STATUS[status])