前端页面创建

This commit is contained in:
Himit_ZH 2020-11-16 23:43:31 +08:00
parent 14171626c7
commit b31b8a9b3c
32 changed files with 3120 additions and 239 deletions

View File

@ -7,11 +7,12 @@
> 开发记录 > 开发记录
| 时间 | 更新内容 | 更新者 | | 时间 | 更新内容 | 更新者 |
| ---------- | ------------------------------------------------------ | -------- | | ---------- | ------------------------------------------------------------ | -------- |
| 2020-10-26 | 数据库设计,登录和注册接口,文档记录开始。 | Himit_ZH | | 2020-10-26 | 数据库设计,登录和注册接口,文档记录开始。 | Himit_ZH |
| 2020-10-28 | 用户模块接口,题目模块接口,比赛模块接口,排行模块接口 | Himit_ZH | | 2020-10-28 | 用户模块接口,题目模块接口,比赛模块接口,排行模块接口 | Himit_ZH |
| 2020-10-30 | 评测模块接口判题服务系统初始化前端vue项目 | Himit_ZH | | 2020-10-30 | 评测模块接口判题服务系统初始化前端vue项目 | Himit_ZH |
| 2020-11-08 | 前端vue主页题目列表页登录注册重置密码弹窗逻辑 | Himit_ZH | | 2020-11-08 | 前端vue主页题目列表页登录注册重置密码弹窗逻辑 | Himit_ZH |
| 2020-11-16 | 前端提交列表页,提交详情页,题目详情页,排行(ACM,OI)页,比赛列表页,个人主页,个人设置页 | Himit_ZH |
# 二、系统架构 # 二、系统架构
@ -301,13 +302,13 @@ jugdeCase表 评测单个样例结果表
contest表 contest表
| 列名 | 实体属性类型 | 键 | 备注 | | 列名 | 实体属性类型 | 键 | 备注 |
| ------------ | ------------ | ----------- | ------------------------------------------- | | ------------ | ------------ | ---- | ----------------------------------------------------- |
| id | long | 主键 | auto_increment 1000起步 | | id | long | 主键 | auto_increment 1000起步 |
| uid | String | 外键 | 创建者id | | uid | String | 外键 | 创建者id |
| title | String | | 比赛标题 | | title | String | | 比赛标题 |
| type | int | | Acm赛制或者Rating | | type | int | | Acm赛制或者Rating |
| source | int | | 比赛来源原创为0克隆赛为比赛id | | source | int | | 比赛来源原创为0克隆赛为比赛id |
| auth | int | | 0为公开赛1为私有赛有密码2为报名赛。 | | auth | int | | 0为公开赛1为私有赛有密码3为保护赛有密码。 |
| pwd | string | | 比赛密码 | | pwd | string | | 比赛密码 |
| start_time | datetime | | 开始时间 | | start_time | datetime | | 开始时间 |
| end_time | datetime | | 结束时间 | | end_time | datetime | | 结束时间 |

View File

@ -1243,6 +1243,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/highlight.js": {
"version": "10.1.0",
"resolved": "https://registry.npm.taobao.org/@types/highlight.js/download/@types/highlight.js-10.1.0.tgz",
"integrity": "sha1-ibsMICmX16kKB70uwffQDFa7kLQ=",
"dev": true,
"requires": {
"highlight.js": "*"
}
},
"@types/http-proxy": { "@types/http-proxy": {
"version": "1.17.4", "version": "1.17.4",
"resolved": "https://registry.npm.taobao.org/@types/http-proxy/download/@types/http-proxy-1.17.4.tgz?cache=0&sync_timestamp=1596839386031&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fhttp-proxy%2Fdownload%2F%40types%2Fhttp-proxy-1.17.4.tgz", "resolved": "https://registry.npm.taobao.org/@types/http-proxy/download/@types/http-proxy-1.17.4.tgz?cache=0&sync_timestamp=1596839386031&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fhttp-proxy%2Fdownload%2F%40types%2Fhttp-proxy-1.17.4.tgz",
@ -3281,6 +3290,12 @@
"integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=", "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
"dev": true "dev": true
}, },
"highlight.js": {
"version": "9.18.3",
"resolved": "https://registry.npm.taobao.org/highlight.js/download/highlight.js-9.18.3.tgz",
"integrity": "sha1-oaCiAo1eMUniOA+Khl7oUWcD1jQ=",
"dev": true
},
"supports-color": { "supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1598611709087&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz", "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1598611709087&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
@ -3298,6 +3313,16 @@
"integrity": "sha1-EnY+RyUb+VHLdcIB36WP8byy0Ec=", "integrity": "sha1-EnY+RyUb+VHLdcIB36WP8byy0Ec=",
"dev": true "dev": true
}, },
"clipboard": {
"version": "2.0.6",
"resolved": "https://registry.npm.taobao.org/clipboard/download/clipboard-2.0.6.tgz?cache=0&sync_timestamp=1599054235610&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fclipboard%2Fdownload%2Fclipboard-2.0.6.tgz",
"integrity": "sha1-UpISlu7A/fd+rRdJQhshyWhkc3Y=",
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"clipboardy": { "clipboardy": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npm.taobao.org/clipboardy/download/clipboardy-2.3.0.tgz", "resolved": "https://registry.npm.taobao.org/clipboardy/download/clipboardy-2.3.0.tgz",
@ -3367,6 +3392,11 @@
"q": "^1.1.2" "q": "^1.1.2"
} }
}, },
"codemirror": {
"version": "5.58.2",
"resolved": "https://registry.npm.taobao.org/codemirror/download/codemirror-5.58.2.tgz?cache=0&sync_timestamp=1603481865446&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcodemirror%2Fdownload%2Fcodemirror-5.58.2.tgz",
"integrity": "sha1-7VSheW3hSYaIvqHN1OnusYdWXRs="
},
"collection-visit": { "collection-visit": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npm.taobao.org/collection-visit/download/collection-visit-1.0.0.tgz", "resolved": "https://registry.npm.taobao.org/collection-visit/download/collection-visit-1.0.0.tgz",
@ -3430,8 +3460,7 @@
"commander": { "commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz?cache=0&sync_timestamp=1603599636161&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcommander%2Fdownload%2Fcommander-2.20.3.tgz", "resolved": "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz?cache=0&sync_timestamp=1603599636161&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcommander%2Fdownload%2Fcommander-2.20.3.tgz",
"integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=", "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM="
"dev": true
}, },
"commondir": { "commondir": {
"version": "1.0.1", "version": "1.0.1",
@ -4181,6 +4210,11 @@
} }
} }
}, },
"default-passive-events": {
"version": "2.0.0",
"resolved": "https://registry.npm.taobao.org/default-passive-events/download/default-passive-events-2.0.0.tgz",
"integrity": "sha1-ebGqZ77LqrOLcYRptUgP75Ltpkk="
},
"defaults": { "defaults": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npm.taobao.org/defaults/download/defaults-1.0.3.tgz", "resolved": "https://registry.npm.taobao.org/defaults/download/defaults-1.0.3.tgz",
@ -4290,6 +4324,11 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true "dev": true
}, },
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npm.taobao.org/delegate/download/delegate-3.2.0.tgz",
"integrity": "sha1-tmtxwxWFIuirV0T3INjKDCr1kWY="
},
"depd": { "depd": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz", "resolved": "https://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz",
@ -4484,6 +4523,14 @@
"safer-buffer": "^2.1.0" "safer-buffer": "^2.1.0"
} }
}, },
"echarts": {
"version": "4.9.0",
"resolved": "https://registry.npm.taobao.org/echarts/download/echarts-4.9.0.tgz?cache=0&sync_timestamp=1605024811446&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fecharts%2Fdownload%2Fecharts-4.9.0.tgz",
"integrity": "sha1-qbm6oD8Doqcx5jQMVb77V6nhNH0=",
"requires": {
"zrender": "4.3.2"
}
},
"ee-first": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz", "resolved": "https://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz",
@ -5371,6 +5418,14 @@
} }
} }
}, },
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npm.taobao.org/good-listener/download/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"requires": {
"delegate": "^3.1.2"
}
},
"graceful-fs": { "graceful-fs": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.2.4.tgz", "resolved": "https://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.2.4.tgz",
@ -5537,10 +5592,9 @@
"dev": true "dev": true
}, },
"highlight.js": { "highlight.js": {
"version": "9.18.3", "version": "10.3.2",
"resolved": "https://registry.npm.taobao.org/highlight.js/download/highlight.js-9.18.3.tgz", "resolved": "https://registry.npm.taobao.org/highlight.js/download/highlight.js-10.3.2.tgz",
"integrity": "sha1-oaCiAo1eMUniOA+Khl7oUWcD1jQ=", "integrity": "sha1-E1/TYZoAw8u4tM1tvHjVa/y8RvE="
"dev": true
}, },
"hmac-drbg": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
@ -6408,6 +6462,14 @@
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },
"katex": {
"version": "0.12.0",
"resolved": "https://registry.npm.taobao.org/katex/download/katex-0.12.0.tgz",
"integrity": "sha1-L7HGZdvSsEPtz4ofXFVfRr6qDLk=",
"requires": {
"commander": "^2.19.0"
}
},
"killable": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz", "resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz",
@ -6486,8 +6548,7 @@
"lodash": { "lodash": {
"version": "4.17.20", "version": "4.17.20",
"resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz",
"integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=", "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI="
"dev": true
}, },
"lodash.defaultsdeep": { "lodash.defaultsdeep": {
"version": "4.6.1", "version": "4.6.1",
@ -7087,6 +7148,11 @@
"path-key": "^2.0.0" "path-key": "^2.0.0"
} }
}, },
"nprogress": {
"version": "0.2.0",
"resolved": "https://registry.npm.taobao.org/nprogress/download/nprogress-0.2.0.tgz?cache=0&sync_timestamp=1587262530340&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnprogress%2Fdownload%2Fnprogress-0.2.0.tgz",
"integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E="
},
"nth-check": { "nth-check": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npm.taobao.org/nth-check/download/nth-check-1.0.2.tgz", "resolved": "https://registry.npm.taobao.org/nth-check/download/nth-check-1.0.2.tgz",
@ -8713,6 +8779,11 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true "dev": true
}, },
"resize-detector": {
"version": "0.1.10",
"resolved": "https://registry.npm.taobao.org/resize-detector/download/resize-detector-0.1.10.tgz",
"integrity": "sha1-HaP5YapfkUzLz9N1LVL9Rb7raSw="
},
"resize-observer-polyfill": { "resize-observer-polyfill": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npm.taobao.org/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz", "resolved": "https://registry.npm.taobao.org/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz",
@ -8849,6 +8920,11 @@
"ajv-keywords": "^3.5.2" "ajv-keywords": "^3.5.2"
} }
}, },
"select": {
"version": "1.1.2",
"resolved": "https://registry.npm.taobao.org/select/download/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"select-hose": { "select-hose": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npm.taobao.org/select-hose/download/select-hose-2.0.0.tgz", "resolved": "https://registry.npm.taobao.org/select-hose/download/select-hose-2.0.0.tgz",
@ -9793,6 +9869,11 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true "dev": true
}, },
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npm.taobao.org/tiny-emitter/download/tiny-emitter-2.1.0.tgz",
"integrity": "sha1-HRpW7fxRxD6GPLtTgqcjMONVVCM="
},
"to-arraybuffer": { "to-arraybuffer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/to-arraybuffer/download/to-arraybuffer-1.0.1.tgz", "resolved": "https://registry.npm.taobao.org/to-arraybuffer/download/to-arraybuffer-1.0.1.tgz",
@ -10272,11 +10353,37 @@
"resolved": "https://registry.npm.taobao.org/vue/download/vue-2.6.12.tgz", "resolved": "https://registry.npm.taobao.org/vue/download/vue-2.6.12.tgz",
"integrity": "sha1-9evU+mvShpQD4pqJau1JBEVskSM=" "integrity": "sha1-9evU+mvShpQD4pqJau1JBEVskSM="
}, },
"vue-clipboard2": {
"version": "0.3.1",
"resolved": "https://registry.npm.taobao.org/vue-clipboard2/download/vue-clipboard2-0.3.1.tgz",
"integrity": "sha1-blUft704SImyiw2jsSKJ7WvKSJQ=",
"requires": {
"clipboard": "^2.0.0"
}
},
"vue-codemirror-lite": {
"version": "1.0.4",
"resolved": "https://registry.npm.taobao.org/vue-codemirror-lite/download/vue-codemirror-lite-1.0.4.tgz",
"integrity": "sha1-SKXNfRfAkUUDyM2dm1a0OOScNBA=",
"requires": {
"codemirror": "^5.22.0"
}
},
"vue-cropper": { "vue-cropper": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npm.taobao.org/vue-cropper/download/vue-cropper-0.5.5.tgz", "resolved": "https://registry.npm.taobao.org/vue-cropper/download/vue-cropper-0.5.5.tgz",
"integrity": "sha1-m9G6Vjx/qiaKvVL7KvTGwo0zyWI=" "integrity": "sha1-m9G6Vjx/qiaKvVL7KvTGwo0zyWI="
}, },
"vue-echarts": {
"version": "5.0.0-beta.0",
"resolved": "https://registry.npm.taobao.org/vue-echarts/download/vue-echarts-5.0.0-beta.0.tgz",
"integrity": "sha1-Q43UsPxczqKBcJwffGMhsFNSvfQ=",
"requires": {
"core-js": "^3.4.4",
"lodash": "^4.17.15",
"resize-detector": "^0.1.10"
}
},
"vue-hot-reload-api": { "vue-hot-reload-api": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npm.taobao.org/vue-hot-reload-api/download/vue-hot-reload-api-2.3.4.tgz", "resolved": "https://registry.npm.taobao.org/vue-hot-reload-api/download/vue-hot-reload-api-2.3.4.tgz",
@ -11258,6 +11365,11 @@
"dev": true "dev": true
} }
} }
},
"zrender": {
"version": "4.3.2",
"resolved": "https://registry.npm.taobao.org/zrender/download/zrender-4.3.2.tgz?cache=0&sync_timestamp=1605029444182&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fzrender%2Fdownload%2Fzrender-4.3.2.tgz",
"integrity": "sha1-7HQy+UFcgsc1hLa3uMR+GwFiCcY="
} }
} }
} }

View File

@ -9,11 +9,19 @@
"dependencies": { "dependencies": {
"axios": "^0.21.0", "axios": "^0.21.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"default-passive-events": "^2.0.0",
"echarts": "^4.9.0",
"element-ui": "^2.14.0", "element-ui": "^2.14.0",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"highlight.js": "^10.3.2",
"katex": "^0.12.0",
"moment": "^2.29.1", "moment": "^2.29.1",
"nprogress": "^0.2.0",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-clipboard2": "^0.3.1",
"vue-codemirror-lite": "^1.0.4",
"vue-cropper": "^0.5.5", "vue-cropper": "^0.5.5",
"vue-echarts": "^5.0.0-beta.0",
"vue-m-message": "^3.0.0", "vue-m-message": "^3.0.0",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",
@ -23,6 +31,7 @@
"xe-utils": "^2.8.1" "xe-utils": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@types/highlight.js": "^10.1.0",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0", "@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0", "@vue/cli-plugin-vuex": "~4.5.0",

View File

@ -25,17 +25,68 @@ export default {
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
} }
body{
background-color: #eee;
font-family: Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,\\5FAE\8F6F\96C5\9ED1,Arial,sans-serif;
-webkit-font-smoothing: antialiased;
color: #495060;
font-size: 12px;
}
code, kbd, pre, samp {
font-family: Consolas,Menlo,Courier,monospace;
}
a { a {
text-decoration: none; text-decoration: none;
background-color: transparent; background-color: transparent;
color: #495060; color: #495060;
transition: all 0.28s ease; outline: 0;
cursor: pointer;
transition: color .2s ease;
} }
a:hover{ a:hover{
color: #2196f3; color: #2196f3;
} }
.drop-menu{
padding-top: 7px;
}
.panel-title{
font-size: 21px;
font-weight: 500;
padding-top: 10px;
padding-bottom: 20px;
line-height: 30px;
}
.status-green{
background-color: #19be6b!important;
color: #fff!important;
}
.status-red{
background-color: #ed3f14!important;
color: #fff!important;
}
.status-yellow{
background-color: #f90!important;
color: #fff!important;
}
.status-blue{
background-color: #2d8cf0!important;
color: #fff!important;
}
.status-gray{
background-color:#909399!important;
color: #fff!important;
}
.own-submit-row{
background:rgb(230, 255, 223) !important;
}
.vxe-table{
color: #495060!important;
font-size: 12px!important;
}
#nprogress .bar {
background: #66B1FF !important;
}
@media screen and (min-width: 1200px) { @media screen and (min-width: 1200px) {
#app { #app {
margin-top: 80px; margin-top: 80px;
@ -54,7 +105,24 @@ export default {
padding: 0 4%; padding: 0 4%;
} }
} }
#problem-content .sample pre {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-ms-flex-item-align: stretch;
align-self: stretch;
border-style: solid;
background: #fafafa;
border-left: 2px solid #3498db;
}
.markdown-body pre {
padding: 5px 10px;
white-space: pre-wrap;
margin-top: 15px;
margin-bottom: 15px;
background: #f8f8f9;
border: 1px dashed #e9eaec;
}
.footer { .footer {
margin-top: 20px; margin-top: 20px;
margin-bottom: 10px; margin-bottom: 10px;
@ -65,8 +133,10 @@ export default {
.fadeInUp-enter-active { .fadeInUp-enter-active {
animation: fadeInUp .8s; animation: fadeInUp .8s;
} }
body{ .el-menu--popup {
background-color: #eee; min-width: 120px!important;
text-align: center;
} }
</style> </style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,5 +1,10 @@
import axios from 'axios' import axios from 'axios'
import mMessage from '@/common/message' import mMessage from '@/common/message'
import NProgress from 'nprogress' // nprogress插件
import 'nprogress/nprogress.css' // nprogress样式
// 配置NProgress进度条选项 —— 动画效果
NProgress.configure({ ease: 'ease', speed: 1000,showSpinner: false})
// 环境的切换 // 环境的切换
if (process.env.NODE_ENV == 'development') { if (process.env.NODE_ENV == 'development') {
@ -14,15 +19,20 @@ if (process.env.NODE_ENV == 'development') {
axios.defaults.timeout = 15000; axios.defaults.timeout = 15000;
axios.interceptors.request.use( axios.interceptors.request.use(
config => { config => {
NProgress.start();
// 每次发送请求之前判断vuex中是否存在token // 每次发送请求之前判断vuex中是否存在token
// 如果存在则统一在http请求的header都加上token这样后台根据token判断你的登录情况 // 如果存在则统一在http请求的header都加上token这样后台根据token判断你的登录情况
// 即使本地存在token也有可能token是过期的所以在响应拦截器中要对返回状态进行判断 // 即使本地存在token也有可能token是过期的所以在响应拦截器中要对返回状态进行判断
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
token && (config.headers.Authorization = token); token && (config.headers.Authorization = token);
return config; return config;
}, },
error => { error => {
NProgress.done();
mmMessage.error(error.response.data.mMessage); mmMessage.error(error.response.data.mMessage);
return Promise.error(error); return Promise.error(error);
}) })
@ -30,6 +40,7 @@ axios.interceptors.request.use(
// 响应拦截器 // 响应拦截器
axios.interceptors.response.use( axios.interceptors.response.use(
response => { response => {
NProgress.done();
if (response.data.status === 200) { if (response.data.status === 200) {
return Promise.resolve(response); return Promise.resolve(response);
} else { } else {
@ -39,6 +50,7 @@ axios.interceptors.response.use(
}, },
// 服务器状态码不是200的情况 // 服务器状态码不是200的情况
error => { error => {
NProgress.done();
if (error.response) { if (error.response) {
switch (error.response.status) { switch (error.response.status) {
// 401: 未登录 // 401: 未登录

View File

@ -1,93 +1,127 @@
export const JUDGE_STATUS = { export const JUDGE_STATUS = {
'-3': {
name: 'Presentation Error',
short: 'PE',
color: 'yellow',
type: 'warning',
rgb:'#f90'
},
'-2': { '-2': {
name: 'Compile Error', name: 'Compile Error',
short: 'CE', short: 'CE',
color: 'yellow', color: 'yellow',
type: 'warning' type: 'warning',
rgb:'#f90'
}, },
'-1': { '-1': {
name: 'Wrong Answer', name: 'Wrong Answer',
short: 'WA', short: 'WA',
color: 'red', color: 'red',
type: 'error' type: 'error',
rgb:'#ed3f14'
}, },
'0': { '0': {
name: 'Accepted', name: 'Accepted',
short: 'AC', short: 'AC',
color: 'green', color: 'green',
type: 'success' type: 'success',
rgb:'#19be6b'
}, },
'1': { '1': {
name: 'Time Limit Exceeded', name: 'Time Limit Exceeded',
short: 'TLE', short: 'TLE',
color: 'red', color: 'red',
type: 'error' type: 'error',
rgb:'#ed3f14'
}, },
'2': { '2': {
name: 'Time Limit Exceeded', name: 'Time Limit Exceeded',
short: 'TLE', short: 'TLE',
color: 'red', color: 'red',
type: 'error' type: 'error',
rgb:'#ed3f14'
}, },
'3': { '3': {
name: 'Memory Limit Exceeded', name: 'Memory Limit Exceeded',
short: 'MLE', short: 'MLE',
color: 'red', color: 'red',
type: 'error' type: 'error',
rgb:'#ed3f14'
}, },
'4': { '4': {
name: 'Runtime Error', name: 'Runtime Error',
short: 'RE', short: 'RE',
color: 'red', color: 'red',
type: 'error' type: 'error',
rgb:'#ed3f14'
}, },
'5': { '5': {
name: 'System Error', name: 'System Error',
short: 'SE', short: 'SE',
color: '#909399', color: 'gray',
type: 'error' type: 'info',
rgb:'#909399'
}, },
'6': { '6': {
name: 'Pending', name: 'Pending',
color: 'yellow', color: 'yellow',
type: 'warning' type: 'warning',
rgb:'#f90'
}, },
'7': { '7': {
name: 'Judging', name: 'Judging',
color: 'blue', color: 'blue',
type: 'info' type: '',
rgb:'#2d8cf0'
}, },
'8': { '8': {
name: 'Partial Accepted', name: 'Partial Accepted',
short: 'PAC', short: 'PAC',
color: 'blue', color: 'blue',
type: 'info' type: '',
rgb:'#2d8cf0'
}, },
'9': { '9': {
name: 'Submitting', name: 'Submitting',
color: 'yellow', color: 'yellow',
type: 'warning' type: 'warning',
rgb:'#f90'
}
}
export const PROBLEM_LEVEL={
'0':{
name:'Easy',
color:'green'
},
'1':{
name:'Mid',
color:'blue'
},
'2':{
name:'Hard',
color:'red'
} }
} }
export const CONTEST_STATUS = { export const CONTEST_STATUS = {
'NOT_START': '1', 'SCHEDULED': '1',
'UNDERWAY': '0', 'RUNNING': '0',
'ENDED': '-1' 'ENDED': '-1'
} }
export const CONTEST_STATUS_REVERSE = { export const CONTEST_STATUS_REVERSE = {
'1': { '1': {
name: 'Not Started', name: 'Scheduled',
color: 'yellow' color: '#f90'
}, },
'0': { '0': {
name: 'Underway', name: 'Running',
color: 'green' color: '#19be6b'
}, },
'-1': { '-1': {
name: 'Ended', name: 'Ended',
color: 'red' color: '#ed3f14'
} }
} }
@ -96,6 +130,30 @@ export const RULE_TYPE = {
OI: 'OI' OI: 'OI'
} }
export const CONTEST_TYPE_REVERSE = {
'0': {
name:'Public',
color:'success',
tips:'公开赛,每个用户都可查看与提交',
submit:true, // 公开赛可看可提交
look:true,
},
'1':{
name:'Private',
color:'danger',
tips:'私有赛,需要密码才可查看与提交',
submit:false, // 私有赛 必须要密码才能看和提交
look:false,
},
'2':{
name:'Protect',
color:'warning',
tips:'保护赛,每个用户都可查看,提交需要密码',
submit:false, //保护赛,可以看但是不能提交,提交需要附带比赛密码
look:true,
}
}
export const CONTEST_TYPE = { export const CONTEST_TYPE = {
PUBLIC: 'Public', PUBLIC: 'Public',
PRIVATE: 'Password Protected' PRIVATE: 'Password Protected'
@ -119,3 +177,10 @@ export const STORAGE_KEY = {
languages: 'languages' languages: 'languages'
} }
export function buildProblemCodeKey (problemID, contestID = null) {
if (contestID) {
return `${STORAGE_KEY.PROBLEM_CODE}_${contestID}_${problemID}`
}
return `${STORAGE_KEY.PROBLEM_CODE}_NaN_${problemID}`
}

View File

@ -0,0 +1,15 @@
import moment from 'moment'
import utils from './utils'
import time from './time'
// 友好显示时间
function fromNow (time) {
return moment(time * 3).fromNow()
}
export default {
submissionMemory: utils.submissionMemoryFormat,
submissionTime: utils.submissionTimeFormat,
localtime: time.utcToLocal,
fromNow: fromNow
}

View File

@ -0,0 +1,26 @@
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-light.css'
export default {
install (Vue, options) {
Vue.directive('highlight', {
deep: true,
bind: function (el, binding) {
Array.from(el.querySelectorAll('code')).forEach((target) => {
if (binding.value) {
target.textContent = binding.value
}
hljs.highlightBlock(target)
})
},
componentUpdated: function (el, binding) {
Array.from(el.querySelectorAll('code')).forEach((target) => {
if (binding.value) {
target.textContent = binding.value
}
hljs.highlightBlock(target)
})
}
})
}
}

View File

@ -0,0 +1,35 @@
import 'katex'
import renderMathInElement from 'katex/contrib/auto-render/auto-render'
import 'katex/dist/katex.min.css'
function _ () {
}
const defaultOptions = {
errorCallback: _,
throwOnError: false,
delimiters: [
{left: '$', right: '$', display: false},
{left: '$$', right: '$$', display: true},
{left: '\\[', right: '\\]', display: true},
{left: '\\(', right: '\\)', display: false}
]
}
function render (el, binding) {
let options = {}
if (binding.value) {
options = binding.value.options || {}
}
Object.assign(options, defaultOptions)
renderMathInElement(el, options)
}
export default {
install: function (Vue, options) {
Vue.directive('katex', {
bind: render,
componentUpdated: render
})
}
}

View File

@ -1,35 +0,0 @@
import Element from 'element-ui';
//全局页面跳转是否启用loading
const routerLoading = true;
//全局api接口调用是否启用loading
const apiLoading = true;
//loading参数配置
const loadingConfig = {
lock: true,
text: 'Loading',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
}
var loading = null ;
const loadingShow = () => {
loading = Element.Loading.service(loadingConfig);
}
const loadingHide = () => {
loading.close();
}
const loadingObj={
loadingShow,
loadingHide,
routerLoading,
apiLoading
}
export default loadingObj

View File

@ -78,6 +78,39 @@ function downloadFile (url) {
}) })
} }
function getLanguages () {
return new Promise((resolve, reject) => {
let languages = storage.get(STORAGE_KEY.languages)
if (languages) {
resolve(languages)
}else{
let langs = [
{
content_type: "text/x-csrc",
description: "GCC 5.4",
name: "C",
},
{
content_type: "text/x-c++src",
description: "G++ 5.4",
name: "C++",
},
{ content_type: "text/x-java",
description: "OpenJDK 1.8",
name: "Java",
},
{
content_type: "text/x-python",
description: "Python 3.7",
name: "Python3",
}
];
storage.set(STORAGE_KEY.languages,langs);
resolve(langs);
}
})
}
export default { export default {
@ -87,4 +120,5 @@ export default {
filterEmptyValue: filterEmptyValue, filterEmptyValue: filterEmptyValue,
breakLongWords: breakLongWords, breakLongWords: breakLongWords,
downloadFile: downloadFile, downloadFile: downloadFile,
getLanguages:getLanguages
} }

View File

@ -0,0 +1,212 @@
<template>
<div style="margin: 0px 0px 15px 0px;font-size: 14px;">
<el-row class="header">
<el-col :xs="24" :md="16" :lg="16">
<div class="select-row">
<span>Langs:</span>
<span>
<el-select :value="this.language" @change="onLangChange" class="adjust" size="small">
<el-option v-for="item in languages" :key="item" :value="item">{{item}}
</el-option>
</el-select>
</span>
<span>
<el-tooltip content="重置代码" placement="top" style="">
<el-button icon="el-icon-refresh" @click="onResetClick" size="small"></el-button>
</el-tooltip>
</span>
<span>
<el-tooltip content="上传文件" placement="top" style="">
<el-button icon="el-icon-upload" @click="onUploadFile" size="small"></el-button>
</el-tooltip>
</span>
<span>
<input type="file" id="file-uploader" style="display: none" @change="onUploadFileDone">
</span>
</div>
</el-col>
<el-col :xs="24" :md="8" :lg="8">
<div class="select-row fl-right">
<span>Theme:</span>
<el-select :value="this.theme" @change="onThemeChange" class="adjust" size="small">
<el-option v-for="item in themes" :key="item.label" :value="item.value">{{item.label}}
</el-option>
</el-select>
</div>
</el-col>
</el-row>
<codemirror :value="value" :options="options" @change="onEditorCodeChange" ref="myEditor">
</codemirror>
</div>
</template>
<script>
import utils from '@/common/utils'
import { codemirror } from 'vue-codemirror-lite'
//
import 'codemirror/theme/monokai.css'
import 'codemirror/theme/solarized.css'
import 'codemirror/theme/material.css'
// mode
import 'codemirror/mode/clike/clike.js'
import 'codemirror/mode/python/python.js'
// active-line.js
import 'codemirror/addon/selection/active-line.js'
// foldGutter
import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/addon/fold/indent-fold.js'
export default {
name: 'CodeMirror',
components: {
codemirror
},
props: {
value: {
type: String,
default: ''
},
languages: {
type: Array,
default: () => {
return ['C', 'C++', 'Java', 'Python3']
}
},
language: {
type: String,
default: 'C++'
},
theme: {
type: String,
default: 'monokai'
}
},
data () {
return {
options: {
// codemirror options
tabSize: 4,
mode: 'text/x-csrc',
theme: 'monokai',
//
lineNumbers: true,
line: true,
//
foldGutter: true,
matchBrackets : true, //
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
//
styleSelectedText: true, //
lineWrapping: true,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true},
},
mode: {
'C++': 'text/x-csrc'
},
themes: [
{label: 'monokai', value: 'monokai'},
{label: 'solarized', value: 'solarized'},
{label: 'material', value: 'material'}
]
}
},
mounted () {
utils.getLanguages().then(languages => {
let mode = {}
languages.forEach(lang => {
mode[lang.name] = lang.content_type
})
this.mode = mode
this.editor.setOption('mode', this.mode[this.language])
})
this.editor.focus()
},
methods: {
onEditorCodeChange (newCode) {
this.$emit('update:value', newCode)
},
onLangChange (newVal) {
this.editor.setOption('mode', this.mode[newVal])
this.$emit('changeLang', newVal)
},
onThemeChange (newTheme) {
this.editor.setOption('theme', newTheme)
this.$emit('changeTheme', newTheme)
},
onResetClick () {
this.$emit('resetCode')
},
onUploadFile () {
document.getElementById('file-uploader').click()
},
onUploadFileDone () {
let f = document.getElementById('file-uploader').files[0]
let fileReader = new window.FileReader()
let self = this
fileReader.onload = function (e) {
var text = e.target.result
self.editor.setValue(text)
document.getElementById('file-uploader').value = ''
}
fileReader.readAsText(f, 'UTF-8')
}
},
computed: {
editor () {
// get current editor object
return this.$refs.myEditor.editor
},
currentLanguage(){
return this.language;
}
},
watch: {
'theme' (newVal, oldVal) {
this.editor.setOption('theme', newVal)
}
}
}
</script>
<style scoped>
.header {
margin-bottom: 10px;
margin-right: 5px;
margin-left: 5px;
}
.header .adjust {
width: 130px;
margin-left: 5px;
}
.select-row{
margin-top: 4px;
}
@media screen and (max-width: 768px) {
.select-row span {
margin-right: 2px;
}
}
@media screen and (min-width: 768px) {
.select-row span {
margin-right: 5px;;
}
.fl-right{
float: right;
}
}
</style>
<style>
.CodeMirror {
height: auto !important;
}
.CodeMirror-scroll {
min-height: 520px;
max-height: 520px;
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<pre v-highlight="code"><code :class="language" :style="styleObject"></code></pre>
</template>
<script>
export default {
name: 'highlight',
data () {
return {
styleObject: {
'border-left': '3px solid '+this.borderColor
}
}
},
props: {
language: {
type: String
},
code: {
required: true,
type: String
},
borderColor: {
type: String,
default: '#19be6b'
}
},
}
</script>
<style scoped>
pre {
padding: 0;
display: block;
}
code {
padding: 20px;
font-size: 1.1em;
}
</style>

View File

@ -4,8 +4,10 @@
<el-menu <el-menu
:default-active="activeIndex" :default-active="activeIndex"
mode="horizontal" mode="horizontal"
:router="isRouter" router
active-text-color="#2196f3" active-text-color="#2196f3"
text-color="#495060"
> >
<div class="logo"> <div class="logo">
<el-image <el-image
@ -26,13 +28,16 @@
<el-menu-item index="/status" <el-menu-item index="/status"
><i class="el-icon-s-marketing"></i>Status</el-menu-item ><i class="el-icon-s-marketing"></i>Status</el-menu-item
> >
<el-menu-item index="/rank" <el-submenu index="rank" >
><i class="el-icon-s-data"></i>Rank</el-menu-item <template slot="title"><i class="el-icon-s-data"></i>Rank</template>
> <el-menu-item index="/acm-rank">ACM Rank</el-menu-item>
<el-menu-item index="/oi-rank">OI Rank</el-menu-item>
</el-submenu>
<el-submenu index="about"> <el-submenu index="about">
<template slot="title"><i class="el-icon-info"></i>About</template> <template slot="title"><i class="el-icon-info"></i>About</template>
<el-menu-item index="6-1">Introduction</el-menu-item> <el-menu-item index="/introduction">Introduction</el-menu-item>
<el-menu-item index="6-2">Developer</el-menu-item> <el-menu-item index="/developer">Developer</el-menu-item>
</el-submenu> </el-submenu>
<template v-if="!isAuthenticated"> <template v-if="!isAuthenticated">
@ -58,7 +63,7 @@
> >
<span class="el-dropdown-link"> <span class="el-dropdown-link">
{{userInfo.username}}<i class="el-icon-arrow-down el-icon--right"></i> {{userInfo.username}}<i class="el-icon-caret-bottom"></i>
</span> </span>
@ -98,12 +103,16 @@ export default {
ResetPwd, ResetPwd,
}, },
mounted() { mounted() {
this.activeIndex = this.$route.path let activeName = this.$route.path.split("/")[1];
if(activeName === ''){
this.activeIndex = '/home'
}else{
this.activeIndex = '/'+activeName
}
}, },
data() { data() {
return { return {
activeIndex: "home", activeIndex: "home",
isRouter:true,
centerDialogVisible: false, centerDialogVisible: false,
imgUrl: require("@/assets/logo.png"), imgUrl: require("@/assets/logo.png"),
}; };
@ -126,9 +135,6 @@ export default {
}, },
computed: { computed: {
...mapGetters(["modalStatus", "userInfo", "isAuthenticated", "isAdminRole","token"]), ...mapGetters(["modalStatus", "userInfo", "isAuthenticated", "isAdminRole","token"]),
activeMenu() {
return "/" + this.$route.path.split("/")[1];
},
modalVisible: { modalVisible: {
get() { get() {
return this.modalStatus.visible; return this.modalStatus.visible;
@ -202,4 +208,14 @@ export default {
line-height: 1em; line-height: 1em;
color: #4e4e4e; color: #4e4e4e;
} }
.el-submenu__title i {
color: #495060!important;
}
.el-menu-item i{
color: #495060;
}
.is-active .el-submenu__title i,.is-active{
color:#2196f3!important;
}
</style> </style>

View File

@ -5,6 +5,7 @@ import store from './store'
import Element from 'element-ui' import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css" import "element-ui/lib/theme-chalk/index.css"
import 'font-awesome/css/font-awesome.min.css' import 'font-awesome/css/font-awesome.min.css'
import 'default-passive-events'
import Message from 'vue-m-message' import Message from 'vue-m-message'
import 'vue-m-message/dist/index.css' import 'vue-m-message/dist/index.css'
import VueCropper from 'vue-cropper' import VueCropper from 'vue-cropper'
@ -14,11 +15,37 @@ import 'xe-utils'
import VXETable from 'vxe-table' import VXETable from 'vxe-table'
import 'vxe-table/lib/style.css' import 'vxe-table/lib/style.css'
Vue.use(VXETable) import Katex from '@/common/katex'
import VueClipboard from 'vue-clipboard2'
import highlight from '@/common/highlight'
import filters from '@/common/filters.js'
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'
Object.keys(filters).forEach(key => { // 注册全局过滤器
Vue.filter(key, filters[key])
})
Vue.use(Katex) // 数学公式渲染
Vue.use(VXETable) // 表格组件
Vue.use(VueClipboard) // 剪贴板
Vue.use(highlight) // 代码高亮
Vue.use(Element) Vue.use(Element)
Vue.use(VueCropper) Vue.use(VueCropper)
Vue.use(Message, { name: 'msg' }) // `Vue.prototype.$msg` Vue.use(Message, { name: 'msg' }) // `Vue.prototype.$msg` 全局消息提示
Vue.component('ECharts', ECharts)
Vue.prototype.$axios = axios Vue.prototype.$axios = axios
Vue.config.productionTip = false Vue.config.productionTip = false
new Vue({ new Vue({

View File

@ -4,8 +4,11 @@ import { sync } from 'vuex-router-sync'
import routes from '@/router/routes' import routes from '@/router/routes'
import mMessage from '@/common/message' import mMessage from '@/common/message'
import store from '@/store' import store from '@/store'
import glo_loading from '@/common/loading' import NProgress from 'nprogress' // nprogress插件
import 'nprogress/nprogress.css' // nprogress样式
// 配置NProgress进度条选项 —— 动画效果
NProgress.configure({ ease: 'ease', speed: 1000,showSpinner: false })
Vue.use(VueRouter) Vue.use(VueRouter)
@ -27,17 +30,15 @@ const router = new VueRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
NProgress.start()
if (to.matched.some(record => record.meta.requireAuth)) { // 判断该路由是否需要登录权限 if (to.matched.some(record => record.meta.requireAuth)) { // 判断该路由是否需要登录权限
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (token) { // 判断当前的token是否存在 登录存入的token if (token) { // 判断当前的token是否存在 登录存入的token
next() next()
} else { } else {
glo_loading.routerLoading ? glo_loading.loadingShow() : '' //如果全局启用页面跳转则加载loading
next({ next({
path: '/' // 无token认证的一致返回到主页 path: '/' // 无token认证的一致返回到主页
}) })
glo_loading.routerLoading ? glo_loading.loadingHide() : ''//关闭loading层
store.commit('changeModalStatus',{mode: 'Login', visible: true}) store.commit('changeModalStatus',{mode: 'Login', visible: true})
mMessage.error('请您先登录!') mMessage.error('请您先登录!')
} }
@ -47,7 +48,7 @@ router.beforeEach((to, from, next) => {
}) })
router.afterEach((to, from, next) => { router.afterEach((to, from, next) => {
NProgress.done()
}) })
sync(store, router) sync(store, router)

View File

@ -5,11 +5,23 @@ import Setting from "@/views/user/Setting.vue"
import ProblemLIst from "@/views/problem/ProblemList.vue" import ProblemLIst from "@/views/problem/ProblemList.vue"
import Logout from "@/views/user/Logout.vue" import Logout from "@/views/user/Logout.vue"
import SubmissionList from "@/views/status/SubmissionList.vue" import SubmissionList from "@/views/status/SubmissionList.vue"
import SubmissionDetails from "@/views/status/SubmissionDetails.vue"
import ContestList from "@/views/contest/ContestList.vue"
import Problem from "@/views/problem/Problem.vue"
import ACMRank from "@/views/rank/ACMRank.vue"
import OIRank from "@/views/rank/OIRank.vue"
import CountDown from "@/views/contest/test.vue"
const routes = [ const routes = [
{
path: '/count-down',
name: 'CountDown',
component: CountDown
},
{ {
path: '/', path: '/',
redirect: {
name: 'Home', name: 'Home',
component: Home }
}, },
{ {
path: '/home', path: '/home',
@ -21,11 +33,36 @@ const routes = [
name: 'ProblemList', name: 'ProblemList',
component: ProblemLIst component: ProblemLIst
}, },
{
path: '/problem/1',
name: 'problem-details',
component: Problem
},
{
path: '/contest',
name: 'ContestList',
component: ContestList
},
{ {
path: '/status', path: '/status',
name: 'SubmissionList', name: 'SubmissionList',
component: SubmissionList component: SubmissionList
}, },
{
path: '/submission-detail',
name: 'SubmissionDeatil',
component: SubmissionDetails
},
{
path: '/acm-rank',
name: 'ACM Rank',
component: ACMRank
},
{
path: '/oi-rank',
name: 'OI Rank',
component: OIRank
},
{ {
path: '/reset-password', path: '/reset-password',
name: 'SetNewPassword', name: 'SetNewPassword',

View File

@ -0,0 +1,184 @@
import moment from 'moment'
import api from '@/common/api'
import { CONTEST_STATUS, USER_TYPE, CONTEST_TYPE } from '@/common/constants'
const state = {
now: moment(),
access: false,
rankLimit: 30,
forceUpdate: false,
contest: {
created_by: {},
contest_type: CONTEST_TYPE.PUBLIC
},
contestProblems: [],
itemVisible: {
menu: true,
chart: true,
realName: false
}
}
const getters = {
// contest 是否加载完成
contestLoaded: (state) => {
return !!state.contest.status
},
contestStatus: (state, getters) => {
if (!getters.contestLoaded) return null
let startTime = moment(state.contest.start_time)
let endTime = moment(state.contest.end_time)
let now = state.now
if (startTime > now) {
return CONTEST_STATUS.NOT_START
} else if (endTime < now) {
return CONTEST_STATUS.ENDED
} else {
return CONTEST_STATUS.UNDERWAY
}
},
contestRuleType: (state) => {
return state.contest.rule_type || null
},
isContestAdmin: (state, getters, _, rootGetters) => {
return rootGetters.isAuthenticated &&
(state.contest.created_by.id === rootGetters.user.id || rootGetters.user.admin_type === USER_TYPE.SUPER_ADMIN)
},
contestMenuDisabled: (state, getters) => {
if (getters.isContestAdmin) return false
if (state.contest.contest_type === CONTEST_TYPE.PUBLIC) {
return getters.contestStatus === CONTEST_STATUS.NOT_START
}
return !state.access
},
OIContestRealTimePermission: (state, getters, _, rootGetters) => {
if (getters.contestRuleType === 'ACM' || getters.contestStatus === CONTEST_STATUS.ENDED) {
return true
}
return state.contest.real_time_rank === true || getters.isContestAdmin
},
problemSubmitDisabled: (state, getters, _, rootGetters) => {
if (getters.contestStatus === CONTEST_STATUS.ENDED) {
return true
} else if (getters.contestStatus === CONTEST_STATUS.NOT_START) {
return !getters.isContestAdmin
}
return !rootGetters.isAuthenticated
},
passwordFormVisible: (state, getters) => {
return state.contest.contest_type !== CONTEST_TYPE.PUBLIC && !state.access && !getters.isContestAdmin
},
contestStartTime: (state) => {
return moment(state.contest.start_time)
},
contestEndTime: (state) => {
return moment(state.contest.end_time)
},
countdown: (state, getters) => {
if (getters.contestStatus === CONTEST_STATUS.NOT_START) {
let duration = moment.duration(getters.contestStartTime.diff(state.now, 'seconds'), 'seconds')
// time is too long
if (duration.weeks() > 0) {
return 'Start At ' + duration.humanize()
}
let texts = [Math.floor(duration.asHours()), duration.minutes(), duration.seconds()]
return '-' + texts.join(':')
} else if (getters.contestStatus === CONTEST_STATUS.UNDERWAY) {
let duration = moment.duration(getters.contestEndTime.diff(state.now, 'seconds'), 'seconds')
let texts = [Math.floor(duration.asHours()), duration.minutes(), duration.seconds()]
return '-' + texts.join(':')
} else {
return 'Ended'
}
}
}
const mutations = {
changeContest (state, payload) {
state.contest = payload.contest
},
changeContestItemVisible(state, payload) {
state.itemVisible = {...state.itemVisible, ...payload}
},
changeRankForceUpdate (state, payload) {
state.forceUpdate = payload.value
},
changeContestProblems(state, payload) {
state.contestProblems = payload.contestProblems
},
changeContestRankLimit(state, payload) {
state.rankLimit = payload.rankLimit
},
contestAccess(state, payload) {
state.access = payload.access
},
clearContest (state) {
state.contest = {created_by: {}}
state.contestProblems = []
state.access = false
state.itemVisible = {
menu: true,
chart: true,
realName: false
}
state.forceUpdate = false
},
now(state, payload) {
state.now = payload.now
},
nowAdd1s (state) {
state.now = moment(state.now.add(1, 's'))
}
}
const actions = {
getContest ({commit, rootState, dispatch}) {
return new Promise((resolve, reject) => {
api.getContest(rootState.route.params.contestID).then((res) => {
resolve(res)
let contest = res.data.data
commit(types.CHANGE_CONTEST, {contest: contest})
commit(types.NOW, {now: moment(contest.now)})
if (contest.contest_type === CONTEST_TYPE.PRIVATE) {
dispatch('getContestAccess')
}
}, err => {
reject(err)
})
})
},
getContestProblems ({commit, rootState}) {
return new Promise((resolve, reject) => {
api.getContestProblemList(rootState.route.params.contestID).then(res => {
res.data.data.sort((a, b) => {
if (a._id === b._id) {
return 0
} else if (a._id > b._id) {
return 1
}
return -1
})
commit(types.CHANGE_CONTEST_PROBLEMS, {contestProblems: res.data.data})
resolve(res)
}, () => {
commit(types.CHANGE_CONTEST_PROBLEMS, {contestProblems: []})
})
})
},
getContestAccess ({commit, rootState}) {
return new Promise((resolve, reject) => {
api.getContestAccess(rootState.route.params.contestID).then(res => {
commit(types.CONTEST_ACCESS, {access: res.data.data.access})
resolve(res)
}).catch()
})
}
}
export default {
state,
mutations,
getters,
actions
}

View File

@ -1,6 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import user from '@/store/user' import user from '@/store/user'
import contest from "@/store/contest"
Vue.use(Vuex) Vue.use(Vuex)
const rootState = { const rootState = {
modalStatus: { modalStatus: {
@ -72,12 +73,20 @@ const rootActions = {
}, },
startTimeOut({ commit }, payload) { startTimeOut({ commit }, payload) {
commit('startTimeOut', payload) commit('startTimeOut', payload)
},
changeDomTitle ({commit, state}, payload) {
if (payload && payload.title) {
window.document.title = state.website.website_name_shortcut + ' | ' + payload.title
} else {
window.document.title = state.website.website_name_shortcut + ' | ' + state.route.meta.title
}
} }
} }
export default new Vuex.Store({ export default new Vuex.Store({
modules: { modules: {
user, user,
contest
}, },
state: rootState, state: rootState,
getters: rootGetters, getters: rootGetters,

View File

@ -9,7 +9,7 @@
<div class="contest-info"> <div class="contest-info">
<div class="contest-tags"> <div class="contest-tags">
<el-button type="primary" round size="mini"><i class="fa fa-calendar"></i> <el-button type="primary" round size="mini"><i class="fa fa-calendar"></i>
{{contest.beginTime | localtime('YYYY-M-D HH:mm') }} {{contest.beginTime | localtime }}
</el-button> </el-button>
<el-button type="success" round size="mini"><i class="fa fa-clock-o"></i> <el-button type="success" round size="mini"><i class="fa fa-clock-o"></i>
{{contest.duration}} {{contest.duration}}
@ -88,6 +88,7 @@
</template> </template>
<script> <script>
import time from "@/common/time";
export default { export default {
name:"home", name:"home",
data() { data() {
@ -96,12 +97,12 @@ export default {
tableData: [{ tableData: [{
oj: 'Codeforces', oj: 'Codeforces',
title: 'Codeforces Round #680 (Div. 1, based on VK Cup 2020-2021 - Final)', title: 'Codeforces Round #680 (Div. 1, based on VK Cup 2020-2021 - Final)',
beginTime: '2016-05-03 12:00:00', beginTime: '2020-11-08T05:00:00Z',
endTime:'2016-05-03 17:00:00', endTime:'2020-11-08T08:00:00Z',
},], },],
contests: [ contests: [
{ {
beginTime:'2020-11-11', beginTime:'2020-11-08T05:00:00Z',
duration:'5hours', duration:'5hours',
type:"ACM", type:"ACM",
description:'<h1>描述</h1>', description:'<h1>描述</h1>',
@ -113,7 +114,12 @@ export default {
goContest(){ goContest(){
} }
} },
filters: {
localtime(value) {
return time.utcToLocal(value);
},
},
} }
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,363 @@
<template>
<el-row type="flex" justify="space-around">
<el-col :span="24">
<el-card shadow>
<div slot="header">
<span class="panel-title"
>{{
query.rule_type === "" ? "All" : query.rule_type
}}
Contests</span
>
<div class="filter-row">
<span>
<el-dropdown
@command="onRuleChange"
placement="bottom"
trigger="hover"
class="drop-menu"
>
<span class="el-dropdown-link">
{{ query.rule_type === "" ? "Rule" : query.rule_type }}
<i class="el-icon-caret-bottom"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="">All</el-dropdown-item>
<el-dropdown-item command="OI">OI</el-dropdown-item>
<el-dropdown-item command="ACM">ACM</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
<span>
<el-dropdown
@on-click="onStatusChange"
placement="bottom"
trigger="hover"
class="drop-menu"
>
<span class="el-dropdown-link">
{{
query.status === ""
? "Status"
: CONTEST_STATUS_REVERSE[query.status].name
}}
<i class="el-icon-caret-bottom"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="">All</el-dropdown-item>
<el-dropdown-item command="0">Running</el-dropdown-item>
<el-dropdown-item command="1">Scheduled</el-dropdown-item>
<el-dropdown-item command="-1">Ended</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
<span>
<vxe-input
v-model="query.keyword"
placeholder="Enter keyword"
type="search"
size="medium"
@search-click="filterByKeyword"
></vxe-input>
</span>
</div>
</div>
<p id="no-contest" v-show="contests.length == 0">暂无比赛</p>
<ol id="contest-list">
<li v-for="contest in contests" :key="contest.title" :style="getborderColor(contest)">
<el-row type="flex" justify="space-between" align="middle">
<el-col :xs="10" :md="2" :lg="2">
<img v-show="contest.rule_type == 'ACM'" class="trophy" :src="acmSrc" width="95px"/>
<img v-show="contest.rule_type == 'OI'" class="trophy" :src="oiSrc" width="95px" />
</el-col>
<el-col :xs="10" :md="20" :lg="20" class="contest-main">
<p class="title">
<a class="entry" @click.stop="toContest(contest)">
{{ contest.title }}
</a>
<template v-if="contest.auth != 0">
<i class="el-icon-lock" size="20"></i>
</template>
</p>
<ul class="detail">
<li>
<i
class="fa fa-calendar"
aria-hidden="true"
style="color: #3091f2"
></i>
{{ contest.start_time | localtime }}
</li>
<li>
<i
class="fa fa-clock-o"
aria-hidden="true"
style="color: #3091f2"
></i>
{{ getDuration(contest.start_time, contest.end_time) }}
</li>
<li>
<el-button
size="mini"
round
@click="onRuleChange(contest.rule_type)"
>
{{ contest.rule_type }}
</el-button>
</li>
<li>
<el-tooltip :content="CONTEST_TYPE_REVERSE[contest.auth].tips" placement="top" effect="light">
<el-tag
:type="CONTEST_TYPE_REVERSE[contest.auth].color"
effect="plain">
{{CONTEST_TYPE_REVERSE[contest.auth].name}}
</el-tag>
</el-tooltip>
</li>
</ul>
</el-col>
<el-col :xs="4" :md="2" :lg="2" style="text-align: center">
<el-tag
effect="dark"
:color="CONTEST_STATUS_REVERSE[contest.status].color"
size="medium"
>
<i
class="fa fa-circle"
aria-hidden="true"
></i>
{{ CONTEST_STATUS_REVERSE[contest.status].name }}
</el-tag>
</el-col>
</el-row>
</li>
</ol>
</el-card>
<Pagination
:total="total"
:pageSize="limit"
@on-change="getContestList"
:current.sync="page"
></Pagination>
</el-col>
</el-row>
</template>
<script>
import api from "@/common/api";
import { mapGetters } from "vuex";
import utils from "@/common/utils";
import Pagination from "@/components/common/Pagination";
import time from "@/common/time";
import { CONTEST_STATUS_REVERSE, CONTEST_TYPE,CONTEST_TYPE_REVERSE } from "@/common/constants";
const limit = 10;
export default {
name: "contest-list",
components: {
Pagination,
},
data() {
return {
page: 1,
query: {
status: "",
keyword: "",
rule_type: "",
},
limit: limit,
total: 0,
rows: "",
contests: [
{
title: "测试比赛",
status: 0,
start_time: "2020-11-08T05:00:00Z",
end_time: "2020-11-08T08:00:00Z",
rule_type: "ACM",
auth: 1,
},
{
title: "测试比赛",
status: 0,
start_time: "2020-11-08T05:00:00Z",
end_time: "2020-11-08T08:00:00Z",
rule_type: "ACM",
auth: 2,
},
{
title: "测试比赛",
status: -1,
start_time: "2020-11-08T05:00:00Z",
end_time: "2020-11-08T08:00:00Z",
rule_type: "ACM",
auth: 0,
},
{
title: "测试比赛",
status: 1,
start_time: "2020-11-08T05:00:00Z",
end_time: "2020-11-08T08:00:00Z",
rule_type: "ACM",
auth: 0,
},
{
title: "测试比赛",
status: 0,
start_time: "2020-11-08T05:00:00Z",
end_time: "2020-11-08T08:00:00Z",
rule_type: "OI",
auth: 0,
},
],
CONTEST_STATUS_REVERSE: CONTEST_STATUS_REVERSE,
// for password modal use
cur_contest_id: "",
CONTEST_TYPE_REVERSE:CONTEST_TYPE_REVERSE,
acmSrc: require("../../assets/acm.jpg"),
oiSrc: require("../../assets/oi.jpg"),
};
},
// beforeRouteEnter(to, from, next) {
// // api.getContestList(0, limit).then(
// // (res) => {
// // next((vm) => {
// // vm.contests = res.data.data.results;
// // vm.total = res.data.data.total;
// // });
// // },
// // (res) => {
// // next();
// // }
// // );
// },
methods: {
init() {
let route = this.$route.query;
this.query.status = route.status || "";
this.query.rule_type = route.rule_type || "";
this.query.keyword = route.keyword || "";
this.page = parseInt(route.page) || 1;
this.getContestList();
},
getContestList(page = 1) {
let offset = (page - 1) * this.limit;
api.getContestList(offset, this.limit, this.query).then((res) => {
this.contests = res.data.data.results;
this.total = res.data.data.total;
});
},
filterByKeyword() {
let query = Object.assign({}, this.query);
query.page = this.page;
this.$router.push({
name: "contest-list",
query: utils.filterEmptyValue(query),
});
},
onRuleChange(rule) {
this.query.rule_type = rule;
this.page = 1;
this.changeRoute();
},
onStatusChange(status) {
this.query.status = status;
this.page = 1;
this.changeRoute();
},
toContest(contest) {
this.cur_contest_id = contest.id;
if (
contest.contest_type !== CONTEST_TYPE.PUBLIC &&
!this.isAuthenticated
) {
this.$error("请先登录");
this.$store.dispatch("changeModalStatus", { visible: true });
} else {
this.$router.push({
name: "contest-details",
params: { contestID: contest.id },
});
}
},
getDuration(startTime, endTime) {
return time.duration(startTime, endTime);
},
getborderColor(contest){
return "border-left: 4px solid "+CONTEST_STATUS_REVERSE[contest.status].color;
}
},
computed: {
...mapGetters(["isAuthenticated", "user"]),
},
watch: {
$route(newVal, oldVal) {
if (newVal !== oldVal) {
this.init();
}
},
},
};
</script>
<style scoped>
#no-contest {
text-align: center;
font-size: 16px;
padding: 20px;
}
.filter-row {
float: right;
}
.el-tag--dark{
border-color: #FFF;
}
@media screen and (max-width: 768px) {
.filter-row span {
margin-right: 2px;
}
}
@media screen and (min-width: 768px) {
.filter-row span {
margin-right: 20px;
}
}
#contest-list > li {
padding: 5px;
margin-left: -20px;
margin-top: 10px;
width: 100%;
border-bottom: 1px solid rgba(187, 187, 187, 0.5);
list-style: none;
}
#contest-list .trophy {
height: 70px;
margin-left: 10px;
margin-right: -20px;
}
#contest-list .contest-main {
text-align: left;
}
#contest-list .contest-main .title {
font-size: 18px;
padding-left: 8px;
margin-bottom: 0;
}
#contest-list .contest-main .title a.entry {
color: #495060;
}
#contest-list .contest-main .title a:hover {
color: #2d8cf0;
border-bottom: 1px solid #2d8cf0;
}
#contest-list .contest-main .detail {
padding-left: 0;
padding-bottom: 10px;
}
#contest-list .contest-main li {
display: inline-block;
padding: 10px 0 0 10px;
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<span>
<slot>{{content}}</slot>
</span>
</template>
<script>
export default {
name: "CountDown",
data() {
return {
timer: null,
date: null,
savedtime: 0, //
hour: null,
min: null,
sec: null,
content: this.endText //
};
},
props: {
// ()
endTime: {
type: Number,
default: ""
},
endText: {
type: String,
default: "0:00:00"
}
},
mounted() {
//
this.timeStart(this.endTime * 60000);
},
methods: {
//
timeStart(endTime) {
this.date = new Date();
var date1 = new Date().getTime(); //
// +3600s Date
this.date.setTime(date1 + endTime);
this.date = this.date.getTime();
//
this.countdowm(this.date);
},
//
timeresume() {
this.timeStart(this.savedtime);
},
//
timepause() {
clearInterval(this.timer);
this.savedtime =
this.hour * 60 * 60 * 1000 + this.min * 60 * 1000 + this.sec * 1000;
},
//
countdowm(timestamp) {
let self = this;
self.timer = setInterval(function() {
let nowTime = new Date();
let endTime = new Date(timestamp * 1);
let t = endTime.getTime() - nowTime.getTime();
// >0
if (t > 0) {
self.hour = Math.floor((t / 3600000) % 24);
self.min = Math.floor((t / 60000) % 60);
self.sec = Math.floor((t / 1000) % 60);
self.$emit("callBack", 1 + self.min + self.hour * 60); // 1
let min = self.min < 10 ? "0" + self.min : self.min;
let sec = self.sec < 10 ? "0" + self.sec : self.sec;
let format = `${self.hour}:${min}:${sec}`;
self.content = format;
} else {
//
self.$emit("callBack", 0);
clearInterval(self.timer);
self.content = "0:00:00";
}
}, 1000);
}
}
};
</script>

View File

@ -0,0 +1,82 @@
<template>
<div class="item">
<div class="open_con">
<el-button
icon="el-icon-switch-button"
circle
:plain="plain"
type="primary"
@click="OPens"
></el-button>
</div>
<div class="slider">
<el-slider v-model="value" :format-tooltip="formatTooltip"></el-slider>
</div>
</div>
</template>
<script>
import countDown from "./count"; //
export default {
  name: "Caeds",
  components: { countDown },
  data() {
    return {
      value: 0,
      plain: true, //  true false
      stop: 0,
      time: "0:00:00"
    };
  },
  mounted() {},
  methods: {
    formatTooltip(val) {
      if (val < 60) {
        if (val < 10) {
          this.time = "0:0" + val + ":00";
          return "0:0" + val + ":00";
        } else {
          this.time = "0:" + val + ":00";
          return "0:" + val + ":00";
        }
      } else {
        if (val < 70) {
          this.time = "1:0" + (val - 60) + ":00";
          return "1:0" + (val - 60) + ":00";
        } else {
          this.time = "1:" + (val - 60) + ":00";
          return "1:" + (val - 60) + ":00";
        }
      }
    },
    // 
    callBack(val) {
      console.log(val);
      this.value = val;
      //  
      if (val == 0) {
        this.stop = 0;
        this.plain = true;
      }
    },
    OPens() {
        if (this.value != 0) {
          if (!this.plain && this.stop == 1) {
            this.stop = 2;
            console.log("stop");
            this.$refs.countdown.timepause();
          }
          if (this.plain && this.stop == 2) {
            this.stop = 1;
            console.log("open");
            this.$refs.countdown.timeresume();
          }
          if (this.stop == 0) {
            this.stop = 1;
          }
        }
        this.plain = !this.plain;
    },
  }
};
</script>

View File

@ -0,0 +1,714 @@
<template>
<div class="flex-container">
<div id="problem-main">
<!--problem main-->
<el-row >
<el-col :sm="24" :md="12" :lg="12" class="problem-detail">
<el-card :padding="10" shadow="always">
<div slot="header" class="panel-title">
<span>{{ problem.title }}</span><br>
<span><el-tag v-for="tag in problem.tags" :key="tag" effect="plain" size="small" style="margin-right:10px;">{{ tag }}</el-tag></span>
<div class="problem-menu">
<span v-if="this.contestID"> <el-link type="primary" :underline="false"><i class="fa fa-home" aria-hidden="true"></i> Contest</el-link></span>
<span> <el-link type="primary" :underline="false" @click="graphVisible = !graphVisible"><i class="fa fa-pie-chart" aria-hidden="true"></i> Statistic</el-link></span>
<span> <el-link type="primary" :underline="false"><i class="fa fa-bars" aria-hidden="true"></i> Submissions</el-link></span>
</div>
<div class="question-intr">
<span>Time Limit{{problem.time_limit}}</span><br>
<span>Memory Limit{{problem.memory_limit}}</span><br>
<span>Level{{problem.difficulty}}</span><span style="margin-left: 10px;">Score{{problem.score}}</span><br>
<span v-show="problem.author">Create By{{problem.author}}</span><br>
</div>
</div>
<div id="problem-content" class="markdown-body" v-katex>
<p class="title">Description</p>
<p class="content" v-html="problem.description"></p>
<!-- {{$t('m.music')}} -->
<p class="title">Input</p>
<p class="content" v-html="problem.input_description"></p>
<p class="title">Output</p>
<p class="content" v-html="problem.output_description"></p>
<div v-for="(sample, index) of problem.samples" :key="index">
<div class="flex-container sample">
<div class="sample-input">
<p class="title">
Sample Input {{ index + 1 }}
<a
class="copy"
v-clipboard:copy="sample.input"
v-clipboard:success="onCopy"
v-clipboard:error="onCopyError"
>
<i class="el-icon-document-copy"></i>
</a>
</p>
<pre>{{ sample.input }}</pre>
</div>
<div class="sample-output">
<p class="title">Sample Output {{ index + 1 }}
<a
class="copy"
v-clipboard:copy="sample.output"
v-clipboard:success="onCopy"
v-clipboard:error="onCopyError"
>
<i class="el-icon-document-copy"></i>
</a>
</p>
<pre>{{ sample.output }}</pre>
</div>
</div>
</div>
<div v-if="problem.hint">
<p class="title">Hint</p>
<el-card dis-hover>
<div class="content" v-html="problem.hint"></div>
</el-card>
</div>
<div v-if="problem.source">
<p class="title">Source</p>
<p class="content">{{ problem.source }}</p>
</div>
</div>
</el-card>
</el-col>
<el-col :sm="24" :md="12" :lg="12" class="submit-detail">
<!--problem main end-->
<el-card :padding="20" id="submit-code" shadow="always">
<CodeMirror
:value.sync="code"
:languages="problem.languages"
:language="language"
:theme="theme"
@resetCode="onResetToTemplate"
@changeTheme="onChangeTheme"
@changeLang="onChangeLang"
></CodeMirror>
<el-row>
<el-col :sm="24" :md="10" :lg="10" style="margin-top:4px;">
<div class="status" v-if="statusVisible">
<template
v-if="
!this.contestID ||
(this.contestID && OIContestRealTimePermission)
"
>
<span>Status</span>
<el-tag
type="dot"
:color="submissionStatus.color"
@click.native="handleRoute('/status/' + submissionId)"
>
{{ submissionStatus.text.replace(/ /g, "_") }}
</el-tag>
</template>
<template
v-else-if="this.contestID && !OIContestRealTimePermission"
>
<el-alert type="success" show-icon effect="dark" :closable="false"
>Submitted successfully</el-alert
>
</template>
</div>
<div v-else-if="problem.my_status === 0">
<el-alert type="success" show-icon effect="dark" :closable="false"
>You have solved the problem</el-alert
>
</div>
<div
v-else-if="
this.contestID &&
!OIContestRealTimePermission &&
submissionExists
"
>
<el-alert type="success" show-icon effect="dark" :closable="false"
>You have submitted a solution</el-alert
>
</div>
<div v-if="contestEnded">
<el-alert type="warning" show-icon effect="dark" :closable="false"
>Contest has ended</el-alert
>
</div>
</el-col>
<el-col :sm="24" :md="14" :lg="14" style="margin-top:4px;">
<template v-if="captchaRequired">
<div class="captcha-container">
<el-tooltip
v-if="captchaRequired"
content="Click to refresh"
placement="top"
>
<img :src="captchaSrc" @click="getCaptchaSrc" />
</el-tooltip>
<el-input v-model="captchaCode" class="captcha-code" />
</div>
</template>
<el-button
type="primary"
icon="el-icon-edit-outline"
:loading="submitting"
@click="submitCode"
:disabled="problemSubmitDisabled || submitted"
class="fl-right"
>
<span v-if="submitting">Submitting</span>
<span v-else>Submit</span>
</el-button>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
</div>
<!-- <div id="right-column">
<VerticalMenu @on-click="handleRoute">
<template v-if="this.contestID">
<VerticalMenu-item :route="{name: 'contest-problem-list', params: {contestID: contestID}}">
<Icon type="ios-photos"></Icon>
{{$t('m.Problems')}}
</VerticalMenu-item>
<VerticalMenu-item :route="{name: 'contest-announcement-list', params: {contestID: contestID}}">
<Icon type="chatbubble-working"></Icon>
{{$t('m.Announcements')}}
</VerticalMenu-item>
</template>
<VerticalMenu-item v-if="!this.contestID || OIContestRealTimePermission" :route="submissionRoute">
<Icon type="navicon-round"></Icon>
{{$t('m.Submissions')}}
</VerticalMenu-item>
<template v-if="this.contestID">
<VerticalMenu-item v-if="!this.contestID || OIContestRealTimePermission"
:route="{name: 'contest-rank', params: {contestID: contestID}}">
<Icon type="stats-bars"></Icon>
{{$t('m.Rankings')}}
</VerticalMenu-item>
<VerticalMenu-item :route="{name: 'contest-details', params: {contestID: contestID}}">
<Icon type="home"></Icon>
{{$t('m.View_Contest')}}
</VerticalMenu-item>
</template>
</VerticalMenu> -->
<el-dialog :visible.sync="graphVisible" width="400px">
<div id="pieChart-detail">
<ECharts :options="largePie" :initOptions="largePieInitOpts"></ECharts>
</div>
<div slot="footer">
<el-button type="ghost" @click="graphVisible=false" size="small">Close</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import CodeMirror from "@/components/common/CodeMirror.vue";
import storage from "@/common/storage";
import {
JUDGE_STATUS,
CONTEST_STATUS,
buildProblemCodeKey,
} from "@/common/constants";
import {pie, largePie} from './chartData'
import api from "@/common/api";
import mMessage from '@/common/message'
//
const filtedStatus = ["-1", "-2", "0", "1", "2", "3", "4", "8"];
export default {
name: "Problem",
components: {
CodeMirror,
},
data() {
return {
statusVisible: false,
captchaRequired: false,
graphVisible: false,
submissionExists: false,
captchaCode: "",
captchaSrc: "",
contestID: "",
problemID: 1000,
submitting: false,
code: "",
language: "C++",
theme: "monokai",
submissionId: "",
submitted: false,
result: {
result: 9,
},
problem: {
title: "A + B Problem",
description:
"<p>请计算两个整数的和并输出结果。</p><p>注意不要有不必要的输出,比如&quot;请输入 a 和 b 的值: &quot;,示例代码见隐藏部分。</p>",
hint:
'<p>C \u8bed\u8a00\u5b9e\u73b0:</p><pre><code class="lang-c++">#include &lt;stdio.h&gt; \nint main(){\n int a, b;\n scanf(&quot;%d%d&quot;, &a, &b);\n printf(&quot;%d\\n&quot;, a+b);\n return 0;\n}</code></pre><p>Java \u5b9e\u73b0:</p><pre><code class="lang-java">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}</code></pre>',
input_description:
"<p>\u4e24\u4e2a\u7528\u7a7a\u683c\u5206\u5f00\u7684\u6574\u6570.</p>",
output_description: "<p>\u4e24\u6570\u4e4b\u548c</p>",
my_status: 0,
time_limit:'1000MS',
memory_limit:'32MB',
score:100,
template: {},
languages: ["C++", "Python3","C"],
author:'Himit_ZH',
rule_type: "OI",
difficulty: "Mid",
source: "\u7ecf\u5178\u9898\u76ee",
samples: [
{
input: "1 1",
output: "2",
},
{
input: "2 3",
output: "5",
},
],
tags: ['简单题','模拟题'],
io_mode: { io_mode: "Standard IO" },
},
pie: pie,
largePie: largePie,
// echarts dom
largePieInitOpts: {
width: '380',
height: '380'
}
};
},
// beforeRouteEnter (to, from, next) {
// let problemCode = storage.get(buildProblemCodeKey(to.params.problemID, to.params.contestID))
// if (problemCode) {
// next(vm => {
// vm.language = problemCode.language
// vm.code = problemCode.code
// vm.theme = problemCode.theme
// })
// } else {
// next()
// }
// },
mounted() {
this.$store.commit("changeContestItemVisible", { menu: false });
this.init();
},
methods: {
...mapActions(["changeDomTitle"]),
init() {
// this.$Loading.start()
this.contestID = this.$route.params.contestID;
this.problemID = this.$route.params.problemID;
let func =
this.$route.name === "problem-details"
? "getProblem"
: "getContestProblem";
api[func](this.problemID, this.contestID).then(
(res) => {
this.$Loading.finish();
let problem = res.data.data;
this.changeDomTitle({ title: problem.title });
api.submissionExists(problem.id).then((res) => {
this.submissionExists = res.data.data;
});
problem.languages = problem.languages.sort();
this.problem = problem;
this.changePie(problem);
// beforeRouteEnter, codetemplate
if (this.code !== "") {
return;
}
// try to load problem template
this.language = this.problem.languages[0];
let template = this.problem.template;
if (template && template[this.language]) {
this.code = template[this.language];
}
},
() => {
this.$Loading.error();
}
);
},
// changePie (problemData) {
// //
// for (let k in problemData.statistic_info) {
// if (filtedStatus.indexOf(k) === -1) {
// delete problemData.statistic_info[k]
// }
// }
// let acNum = problemData.accepted_number
// let data = [
// {name: 'WA', value: problemData.submission_number - acNum},
// {name: 'AC', value: acNum}
// ]
// this.pie.series[0].data = data
// // AC selecteddeepcopy
// let data2 = JSON.parse(JSON.stringify(data))
// data2[1].selected = true
// this.largePie.series[1].data = data2
// // legend,legend
// let legend = Object.keys(problemData.statistic_info).map(ele => JUDGE_STATUS[ele].short)
// if (legend.length === 0) {
// legend.push('AC', 'WA')
// }
// this.largePie.legend.data = legend
// // ac
// let acCount = problemData.statistic_info['0']
// delete problemData.statistic_info['0']
// let largePieData = []
// Object.keys(problemData.statistic_info).forEach(ele => {
// largePieData.push({name: JUDGE_STATUS[ele].short, value: problemData.statistic_info[ele]})
// })
// largePieData.push({name: 'AC', value: acCount})
// this.largePie.series[0].data = largePieData
// },
handleRoute(route) {
this.$router.push(route);
},
onChangeLang(newLang) {
if (this.problem.template[newLang]) {
if (this.code.trim() === "") {
this.code = this.problem.template[newLang];
}
}
this.language = newLang;
},
onChangeTheme(newTheme) {
this.theme = newTheme;
},
onResetToTemplate() {
this.$confirm("你是否确定要重置你的代码?", "提示", {
cancelButtonText: "取消",
confirmButtonText: "确定",
type: "warning",
})
.then(() => {
let template = this.problem.template;
if (template && template[this.language]) {
this.code = template[this.language];
} else {
this.code = "";
}
})
.catch(() => {});
},
checkSubmissionStatus() {
// 使setTimeout
if (this.refreshStatus) {
// ,,timeout
clearTimeout(this.refreshStatus);
}
const checkStatus = () => {
let id = this.submissionId;
api.getSubmission(id).then(
(res) => {
this.result = res.data.data;
if (Object.keys(res.data.data.statistic_info).length !== 0) {
this.submitting = false;
this.submitted = false;
clearTimeout(this.refreshStatus);
this.init();
} else {
this.refreshStatus = setTimeout(checkStatus, 2000);
}
},
(res) => {
this.submitting = false;
clearTimeout(this.refreshStatus);
}
);
};
this.refreshStatus = setTimeout(checkStatus, 2000);
},
submitCode() {
if (this.code.trim() === "") {
this.$error("m.Code_can_not_be_empty");
return;
}
this.submissionId = "";
this.result = { result: 9 };
this.submitting = true;
let data = {
problem_id: this.problem.id,
language: this.language,
code: this.code,
contest_id: this.contestID,
};
if (this.captchaRequired) {
data.captcha = this.captchaCode;
}
const submitFunc = (data, detailsVisible) => {
this.statusVisible = true;
api.submitCode(data).then(
(res) => {
this.submissionId = res.data.data && res.data.data.submission_id;
//
this.submitting = false;
this.submissionExists = true;
if (!detailsVisible) {
this.$Modal.success({
title: "Success",
content: "Submit_code_successfully",
});
return;
}
this.submitted = true;
this.checkSubmissionStatus();
},
(res) => {
this.getCaptchaSrc();
if (res.data.data.startsWith("Captcha is required")) {
this.captchaRequired = true;
}
this.submitting = false;
this.statusVisible = false;
}
);
};
if (this.contestRuleType === "OI" && !this.OIContestRealTimePermission) {
if (this.submissionExists) {
this.$Modal.confirm({
title: "",
content:
"<h3>" +
"You_have_submission_in_this_problem_sure_to_cover_it" +
"<h3>",
onOk: () => {
// (
setTimeout(() => {
submitFunc(data, false);
}, 1000);
},
onCancel: () => {
this.submitting = false;
},
});
} else {
submitFunc(data, false);
}
} else {
submitFunc(data, true);
}
},
onCopy(event) {
mMessage.success('Sample copied successfully');
},
onCopyError(e) {
mMessage.success('Sample copy failed');
},
},
computed: {
...mapGetters([
"problemSubmitDisabled",
"contestRuleType",
"OIContestRealTimePermission",
"contestStatus",
]),
contest() {
return this.$store.state.contest.contest;
},
contestEnded() {
return this.contestStatus === CONTEST_STATUS.ENDED;
},
submissionStatus() {
return {
text: JUDGE_STATUS[this.result.result]["name"],
color: JUDGE_STATUS[this.result.result]["color"],
};
},
submissionRoute() {
if (this.contestID) {
return {
name: "contest-submission-list",
query: { problemID: this.problemID },
};
} else {
return {
name: "submission-list",
query: { problemID: this.problemID },
};
}
},
},
beforeRouteLeave(to, from, next) {
//
clearInterval(this.refreshStatus);
this.$store.commit("changeContestItemVisible", { menu: true });
storage.set(buildProblemCodeKey(this.problem._id, from.params.contestID), {
code: this.code,
language: this.language,
theme: this.theme,
});
next();
},
watch: {
$route() {
this.init();
},
},
};
</script>
<style scoped>
#problem-main {
flex: auto;
}
.problem-menu{
float: right;
}
.problem-menu span{
margin-left: 10px;
}
.el-link{
font-size: 16px!important;
}
.question-intr {
margin-top: 30px;
border-radius: 4px;
border: 1px solid #ddd;
border-left: 2px solid #3498db;
background: #fafafa;
padding: 10px;
line-height: 1.8;
margin-bottom: 10px;
font-size: 14px;
}
@media screen and (min-width: 768px) {
.problem-detail {
height: 672px;
overflow-y: auto;
}
.problem-menu span{
margin-left: 20px;
}
.question-intr{
margin-top: 5px;
}
}
.submit-detail{
height:100%
}
/deep/ .el-card__header {
border-bottom: 0px;
padding-bottom: 0px;
}
#right-column {
flex: none;
width: 220px;
}
#problem-content {
margin-top: -50px;
}
#problem-content .title {
font-size: 20px;
font-weight: 400;
margin: 25px 0 8px 0;
color: #3091f2;
}
#problem-content .copy {
padding-left: 8px;
}
p.content {
margin-left: 25px;
margin-right: 20px;
font-size: 15px;
}
.flex-container {
display: flex;
width: 100%;
max-width: 100%;
justify-content: space-around;
align-items: flex-start;
flex-flow: row nowrap;
}
.sample {
align-items: stretch;
}
.sample-input,
.sample-output {
width: 50%;
flex: 1 1 auto;
display: flex;
flex-direction: column;
margin-right: 5%;
}
.sample pre {
flex: 1 1 auto;
align-self: stretch;
border-style: solid;
background: transparent;
}
/* #submit-code {
margin-top: 20px;
margin-bottom: 20px;
} */
#submit-code .status {
float: left;
}
#submit-code .status span {
margin-right: 10px;
margin-left: 10px;
}
.captcha-container {
display: inline-block;
}
.captcha-container .captcha-code {
width: auto;
margin-top: -20px;
margin-left: 20px;
}
.fl-right {
float: right;
}
/deep/.el-dialog__body {
padding: 10px 10px!important;
}
#pieChart .echarts {
height: 250px;
width: 210px;
}
#pieChart #detail {
position: absolute;
right: 10px;
top: 10px;
}
/deep/.echarts {
width: 350px;
height: 350px;
}
#pieChart-detail {
/* margin-top: 20px; */
height: 350px;
}
</style>

View File

@ -16,7 +16,7 @@
> >
<span class="el-dropdown-link"> <span class="el-dropdown-link">
{{query.difficulty === '' ? 'Difficulty' : query.difficulty}} {{query.difficulty === '' ? 'Difficulty' : query.difficulty}}
<i class="el-icon-arrow-down el-icon--right"></i> <i class="el-icon-caret-bottom"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="All">All</el-dropdown-item> <el-dropdown-item command="All">All</el-dropdown-item>
@ -37,9 +37,24 @@
@cell-mouseenter="cellHover" @cell-mouseenter="cellHover"
:data="problemList"> :data="problemList">
<vxe-table-column field="pid" title="Problem ID" width="100"></vxe-table-column> <vxe-table-column field="pid" title="Problem ID" width="100"></vxe-table-column>
<vxe-table-column field="title" title="Title" width="300" type="html"></vxe-table-column>
<vxe-table-column field="level" title="Level" width="150" type="html"></vxe-table-column> <vxe-table-column field="title" title="Title" width="300">
<vxe-table-column field="tag" title="Tag" width="250" type="html"></vxe-table-column> <template v-slot="{ row }">
<a :href="getProblemUri(row.pid)" style="color:rgb(87, 163, 243);">{{row.title}}</a>
</template>
</vxe-table-column>
<vxe-table-column field="level" title="Level" width="150">
<template v-slot="{ row }">
<span :class="getLevelColor(row.level)">{{PROBLEM_LEVEL[row.level].name}}</span>
</template>
</vxe-table-column>
<vxe-table-column field="tag" title="Tag" width="250" type="html">
<template v-slot="{ row }">
<span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;" v-for="tag in row.tags" :key="tag">{{tag}}</span>
</template>
</vxe-table-column>
<vxe-table-column field="total" title="Total" width="100"></vxe-table-column> <vxe-table-column field="total" title="Total" width="100"></vxe-table-column>
<vxe-table-column field="ACRate" title="AC Rate" width="100"></vxe-table-column> <vxe-table-column field="ACRate" title="AC Rate" width="100"></vxe-table-column>
</vxe-table> </vxe-table>
@ -51,68 +66,12 @@
<el-col :sm="24" :md="6" :lg="6"> <el-col :sm="24" :md="6" :lg="6">
<el-card style="text-align:center"> <el-card style="text-align:center">
<span class="panel-title" >题目</span> <span class="panel-title" >题目</span>
<el-row> <el-row v-for="record in problemRecord" :key="record">
<el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;"> <el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;">
<el-tag type="success" effect="dark" size="small">AC</el-tag> <el-tag effect="dark" size="small" :color="JUDGE_STATUS[record.status].rgb">{{JUDGE_STATUS[record.status].short}}</el-tag>
</el-col> </el-col>
<el-col :xs="19" :sm="20" :md="18" :lg="20" > <el-col :xs="19" :sm="20" :md="18" :lg="20" >
<el-progress :text-inside="true" :stroke-width="20" :percentage="70" status="success"></el-progress> <el-progress :text-inside="true" :stroke-width="20" :percentage="record.count" :color="JUDGE_STATUS[record.status].rgb"></el-progress>
</el-col>
</el-row>
<el-row>
<el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;">
<el-tag type="danger" effect="dark" size="small">WA</el-tag>
</el-col>
<el-col :xs="19" :sm="20" :md="18" :lg="20" >
<el-progress :text-inside="true" :stroke-width="20" :percentage="70" status="exception"></el-progress>
</el-col>
</el-row>
<el-row>
<el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;">
<el-tag type="danger" effect="dark" size="small">MLE</el-tag>
</el-col>
<el-col :xs="19" :sm="20" :md="18" :lg="20" >
<el-progress :text-inside="true" :stroke-width="20" :percentage="70" status="exception"></el-progress>
</el-col>
</el-row>
<el-row>
<el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;">
<el-tag type="danger" effect="dark" size="small">TLE</el-tag>
</el-col>
<el-col :xs="19" :sm="20" :md="18" :lg="20" >
<el-progress :text-inside="true" :stroke-width="20" :percentage="70" status="exception"></el-progress>
</el-col>
</el-row>
<el-row>
<el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;">
<el-tag type="danger" effect="dark" size="small">RTE</el-tag>
</el-col>
<el-col :xs="19" :sm="20" :md="18" :lg="20" >
<el-progress :text-inside="true" :stroke-width="20" :percentage="70" status="exception"></el-progress>
</el-col>
</el-row>
<el-row>
<el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;">
<el-tag type="warning" effect="dark" size="small">PE</el-tag>
</el-col>
<el-col :xs="19" :sm="20" :md="18" :lg="20" >
<el-progress :text-inside="true" :stroke-width="20" :percentage="70" status="warning"></el-progress>
</el-col>
</el-row>
<el-row>
<el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;">
<el-tag type="warning" effect="dark" size="small">CE</el-tag>
</el-col>
<el-col :xs="19" :sm="20" :md="18" :lg="20" >
<el-progress :text-inside="true" :stroke-width="20" :percentage="70" status="warning"></el-progress>
</el-col>
</el-row>
<el-row>
<el-col :xs="5" :sm="4" :md="6" :lg="4" style="margin-top: 10px;">
<el-tag type="info" effect="dark" size="small">SE</el-tag>
</el-col>
<el-col :xs="19" :sm="20" :md="18" :lg="20" >
<el-progress :text-inside="true" :stroke-width="20" :percentage="70" :color="SEcolor"></el-progress>
</el-col> </el-col>
</el-row> </el-row>
</el-card> </el-card>
@ -140,6 +99,7 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import api from '@/common/api' import api from '@/common/api'
import utils from '@/common/utils' import utils from '@/common/utils'
import { PROBLEM_LEVEL,JUDGE_STATUS } from '@/common/constants'
import Pagination from '@/components/common/Pagination' import Pagination from '@/components/common/Pagination'
export default { export default {
@ -150,41 +110,33 @@
data () { data () {
return { return {
SEcolor:'#909399', SEcolor:'#909399',
PROBLEM_LEVEL:'',
JUDGE_STATUS:'',
tagList: [{ tagList: [{
id:'12', id:'12',
name:'模拟题' name:'模拟题'
}, }],
{ problemRecord:[
id:'13', {status:'0',count:'70'},
name:'模拟题' {status:'-1',count:'70'},
}, {status:'3',count:'70'},
{ {status:'1',count:'70'},
id:'14', {status:'4',count:'70'},
name:'模拟题' {status:'-3',count:'70'},
}, {status:'-2',count:'70'},
{ {status:'5',count:'70'},
id:'15', ],
name:'模拟题'
},
{
id:'16',
name:'模拟题'
},],
problemList: [ problemList: [
{pid:'1000',title:'<a href="https://github.com/x-extends/vxe-table" class="title-link">测试标题</a>',level:'<span class="el-tag el-tag--dark el-tag--small">Easy</span>', {pid:'1000',title:'测试标题',level:0,
tag:'<span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;">简单题</span><span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;">简单题</span>', tags:['简单题','模拟题'],
total:'10000',ACRate:'59.12%' total:'10000',ACRate:'59.12%'
}, },
{pid:'1000',title:'<a href="https://github.com/x-extends/vxe-table" class="title-link">测试标题</a>',level:'<span class="el-tag el-tag--dark el-tag--small">Easy</span>', {pid:'1000',title:'测试标题',level:1,
tag:'<span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;">简单题</span><span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;">简单题</span>', tags:['简单题','模拟题'],
total:'10000',ACRate:'59.12%' total:'10000',ACRate:'59.12%'
}, },
{pid:'1000',title:'<a href="https://github.com/x-extends/vxe-table" class="title-link">测试标题</a>',level:'<span class="el-tag el-tag--dark el-tag--small">Easy</span>', {pid:'1000',title:'测试标题',level:2,
tag:'<span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;">简单题</span><span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;">简单题</span>', tags:['简单题','模拟题'],
total:'10000',ACRate:'59.12%'
},
{pid:'1000',title:'<a href="https://github.com/x-extends/vxe-table" class="title-link">测试标题</a>',level:'<span class="el-tag el-tag--dark el-tag--small">Easy</span>',
tag:'<span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;">简单题</span><span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;">简单题</span>',
total:'10000',ACRate:'59.12%' total:'10000',ACRate:'59.12%'
}, },
], ],
@ -210,7 +162,9 @@
} }
}, },
mounted () { mounted () {
this.init() this.init();
this.PROBLEM_LEVEL = Object.assign({}, PROBLEM_LEVEL);
this.JUDGE_STATUS = Object.assign({}, JUDGE_STATUS)
}, },
methods: { methods: {
init (simulate = false) { init (simulate = false) {
@ -279,6 +233,12 @@
this.$success('Good Luck') this.$success('Good Luck')
this.$router.push({name: 'problem-details', params: {problemID: res.data.data}}) this.$router.push({name: 'problem-details', params: {problemID: res.data.data}})
}) })
},
getProblemUri(pid){
return '/problem/'+pid;
},
getLevelColor(level){
return 'el-tag el-tag--small status-'+PROBLEM_LEVEL[level].color;
} }
}, },
computed: { computed: {
@ -305,7 +265,9 @@
font-size: 21px; font-size: 21px;
font-weight: 500; font-weight: 500;
} }
/deep/.el-tag--dark {
border-color: #d9ecff;
}
/deep/.tag-btn { /deep/.tag-btn {
margin-left: 4px!important; margin-left: 4px!important;
margin-top: 4px; margin-top: 4px;
@ -327,13 +289,6 @@
margin-top:5px; margin-top:5px;
} }
} }
.panel-title{
font-size: 21px;
font-weight: 500;
padding-top: 10px;
padding-bottom: 20px;
line-height: 30px;
}
ul{ ul{
float: right; float: right;
} }

View File

@ -0,0 +1,120 @@
const pieColorMap = {
'AC': {color: '#19be6b'},
'WA': {color: '#ed3f14'},
'TLE': {color: '#ff9300'},
'MLE': {color: '#f7de00'},
'RE': {color: '#ff6104'},
'CE': {color: '#80848f'},
'PAC': {color: '#2d8cf0'}
}
function getItemColor (obj) {
return pieColorMap[obj.name].color
}
const pie = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
left: 'center',
top: '10',
orient: 'horizontal',
data: ['AC', 'WA']
},
series: [
{
name: 'Summary',
type: 'pie',
radius: '60%',
center: ['53%', '55%'],
itemStyle: {
normal: {color: getItemColor}
},
data: [
{value: 0, name: 'WA'},
{value: 0, name: 'AC'}
],
label: {
normal: {
position: 'inner',
show: true,
formatter: '{b}: {c}\n {d}%',
textStyle: {
fontWeight: 'bold'
}
}
}
}
]
}
const largePie = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
left: 'center',
top:0,
orient:
'horizontal',
itemGap:
10,
data:
['AC', 'RE', 'WA', 'TLE', 'PAC', 'MLE']
},
series: [
{
name: 'Detail',
type: 'pie',
radius: ['50%', '65%'],
center: ['50%', '55%'],
itemStyle: {
normal: {color: getItemColor}
},
data: [
{value: 0, name: 'RE'},
{value: 0, name: 'WA'},
{value: 0, name: 'TLE'},
{value: 0, name: 'AC'},
{value: 0, name: 'MLE'},
{value: 0, name: 'PAC'}
],
label: {
normal: {
formatter: '{b}: \n{d}%\n {c}',
textStyle:{
fontSize:10,
fontWeight: 'bold'
}
}
},
labelLine: {
normal: {}
}
},
{
name: 'Summary',
type: 'pie',
radius: '35%',
center: ['52%', '55%'],
itemStyle: {
normal: {color: getItemColor}
},
data: [
{value: 0, name: 'WA'},
{value: 0, name: 'AC', selected: true}
],
label: {
normal: {
position: 'inner',
formatter: '{b}: {c}\n {d}%'
}
}
}
]
}
export { pie, largePie }

View File

@ -0,0 +1,167 @@
<template>
<el-row type="flex" justify="space-around">
<el-col :span="22">
<el-card :padding="10">
<div slot="header"><span class="panel-title">ACM Ranklist</span></div>
<div class="echarts">
<ECharts :options="options" ref="chart" auto-resize></ECharts>
</div>
</el-card>
<vxe-table :data="dataRank" :loading="loadingTable" align="center" highlight-hover-row style="font-weight: 500;">
<vxe-table-column type="seq" width="100"></vxe-table-column>
<vxe-table-column field="username" title="User" width="197">
<template v-slot="{ row }">
<a :href="getInfoByUsername(row.username)" style="color:rgb(87, 163, 243);">{{row.username}}</a>
</template>
</vxe-table-column>
<vxe-table-column field="nickname" title="Nickname" width="197"></vxe-table-column>
<vxe-table-column field="signature" title="Mood" width="197"></vxe-table-column>
<vxe-table-column field="ac" title="AC" width="197"></vxe-table-column>
<vxe-table-column field="total" title="Total" width="197"></vxe-table-column>
<vxe-table-column field="rating" title="Rating" width="197"></vxe-table-column>
</vxe-table>
<Pagination :total="total" :page-size.sync="limit" :current.sync="page"
@on-change="getRankData" show-sizer
@on-page-size-change="getRankData(1)"></Pagination>
</el-col>
</el-row>
</template>
<script>
import api from '@/common/api'
import Pagination from '@/components/common/Pagination'
import utils from '@/common/utils'
import { RULE_TYPE } from '@/common/constants'
export default {
name: 'acm-rank',
components: {
Pagination
},
data () {
return {
page: 1,
limit: 30,
total: 0,
loadingTable: false,
dataRank: [
{username:'root',nickname:'Himit_ZH',signature:'Show me your code',ac:100,total:100,rating:'100%'},
{username:'root',nickname:'Himit_ZH',signature:'Show me your code',ac:100,total:100,rating:'100%'},
{username:'root',nickname:'Himit_ZH',signature:'Show me your code',ac:100,total:100,rating:'100%'},
],
options: {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['AC', 'Total']
},
grid: {
x: '3%',
x2: '3%'
},
toolbox: {
show: true,
feature: {
dataView: {show: true, readOnly: true},
magicType: {show: true, type: ['line', 'bar', 'stack']},
saveAsImage: {show: true}
},
right: '5%',
top:'5%'
},
calculable: true,
xAxis: [
{
type: 'category',
data: ['root'],
axisLabel: {
interval: 0,
showMinLabel: true,
showMaxLabel: true,
align: 'center',
formatter: (value, index) => {
return utils.breakLongWords(value, 10)
}
}
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'AC',
type: 'bar',
data: [0],
markPoint: {
data: [
{type: 'max', name: 'max'}
]
}
},
{
name: 'Total',
type: 'bar',
data: [0],
markPoint: {
data: [
{type: 'max', name: 'max'}
]
}
}
]
}
}
},
mounted () {
// this.getRankData(1)
},
methods: {
getRankData (page) {
let offset = (page - 1) * this.limit
let bar = this.$refs.chart
bar.showLoading({maskColor: 'rgba(250, 250, 250, 0.8)'})
this.loadingTable = true
api.getUserRank(offset, this.limit, RULE_TYPE.ACM).then(res => {
this.loadingTable = false
if (page === 1) {
this.changeCharts(res.data.data.results.slice(0, 10))
}
this.total = res.data.data.total
this.dataRank = res.data.data.results
bar.hideLoading()
}).catch(() => {
this.loadingTable = false
bar.hideLoading()
})
},
changeCharts (rankData) {
let [usernames, acData, totalData] = [[], [], []]
rankData.forEach(ele => {
usernames.push(ele.user.username)
acData.push(ele.accepted_number)
totalData.push(ele.submission_number)
})
this.options.xAxis[0].data = usernames
this.options.series[0].data = acData
this.options.series[1].data = totalData
},
getInfoByUsername(username){
return '/user-home/'+username;
}
}
}
</script>
<style scoped>
.echarts {
margin: 0 auto;
width: 95%;
height: 400px;
}
</style>

View File

@ -0,0 +1,153 @@
<template>
<el-row type="flex" justify="space-around">
<el-col :span="22">
<el-card :padding="10">
<div slot="header"><span class="panel-title">OI Ranklist</span></div>
<div class="echarts">
<ECharts :options="options" ref="chart" auto-resize></ECharts>
</div>
</el-card>
<vxe-table :data="dataRank" :loading="loadingTable" align="center" highlight-hover-row style="font-weight: 500;">
<vxe-table-column type="seq" width="100"></vxe-table-column>
<vxe-table-column field="username" title="User" width="168">
<template v-slot="{ row }">
<a :href="getInfoByUsername(row.username)" style="color:rgb(87, 163, 243);">{{row.username}}</a>
</template>
</vxe-table-column>
<vxe-table-column field="nickname" title="Nickname" width="168"></vxe-table-column>
<vxe-table-column field="signature" title="Mood" width="173"></vxe-table-column>
<vxe-table-column field="score" title="Score" width="168"></vxe-table-column>
<vxe-table-column field="ac" title="AC" width="168"></vxe-table-column>
<vxe-table-column field="total" title="Total" width="168"></vxe-table-column>
<vxe-table-column field="rating" title="Rating" width="168"></vxe-table-column>
</vxe-table>
<Pagination :total="total" :page-size.sync="limit" :current.sync="page"
@on-change="getRankData"
show-sizer @on-page-size-change="getRankData(1)"></Pagination>
</el-col>
</el-row>
</template>
<script>
import api from '@/common/api'
import Pagination from '@/components/common/Pagination'
import utils from '@/common/utils'
import { RULE_TYPE } from '@/common/constants'
export default {
name: 'acm-rank',
components: {
Pagination
},
data () {
return {
page: 1,
limit: 30,
total: 0,
dataRank: [
{username:'root',nickname:'Himit_ZH',signature:'Show me your code',score:10000,ac:100,total:100,rating:'100%'},
{username:'root',nickname:'Himit_ZH',signature:'Show me your code',score:10000,ac:100,total:100,rating:'100%'},
{username:'root',nickname:'Himit_ZH',signature:'Show me your code',score:10000,ac:100,total:100,rating:'100%'},
],
options: {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['Score']
},
grid: {
x: '3%',
x2: '3%'
},
toolbox: {
show: true,
feature: {
dataView: {show: true, readOnly: true},
magicType: {show: true, type: ['line', 'bar']},
saveAsImage: {show: true}
},
right: '5%',
top:'5%'
},
calculable: true,
xAxis: [
{
type: 'category',
data: ['root'],
boundaryGap: true,
axisLabel: {
interval: 0,
showMinLabel: true,
showMaxLabel: true,
align: 'center',
formatter: (value, index) => {
return utils.breakLongWords(value, 14)
}
},
axisTick: {
alignWithLabel: true
}
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'Score',
type: 'bar',
data: [0],
barMaxWidth: '80',
markPoint: {
data: [
{type: 'max', name: 'max'}
]
}
}
]
}
}
},
mounted () {
// this.getRankData(1)
},
methods: {
getRankData (page) {
let offset = (page - 1) * this.limit
let bar = this.$refs.chart
bar.showLoading({maskColor: 'rgba(250, 250, 250, 0.8)'})
api.getUserRank(offset, this.limit, RULE_TYPE.OI).then(res => {
if (page === 1) {
this.changeCharts(res.data.data.results.slice(0, 10))
}
this.total = res.data.data.total
this.dataRank = res.data.data.results
bar.hideLoading()
})
},
changeCharts (rankData) {
let [usernames, scores] = [[], []]
rankData.forEach(ele => {
usernames.push(ele.user.username)
scores.push(ele.total_score)
})
this.options.xAxis[0].data = usernames
this.options.series[0].data = scores
},
getInfoByUsername(username){
return '/user-home/'+username;
}
}
}
</script>
<style scoped>
.echarts {
margin: 0 auto;
width: 95%;
height: 400px;
}
</style>

View File

@ -0,0 +1,267 @@
<template>
<el-row type="flex" justify="space-around">
<el-col :span="22" id="status">
<el-alert :type="status.type" show-icon :closable="false" effect="dark" :class="getbackgroudColor(submission.data[0].status)"
style="padding: 18px;">
<template slot="title">
<span class="title">{{ status.statusName }}</span>
</template>
<template slot>
<div v-if="isCE" class="content">
<pre>{{ submission.data[0].err_info }}</pre>
</div>
<div v-else class="content">
<span class="span-row">Time: {{ submission.data[0].time }}</span>
<span class="span-row">Memory: {{ submission.data[0].memory }}</span>
<span class="span-row">Language: {{ submission.data[0].language }}</span>
<span class="span-row">Author: {{ submission.data[0].author }}</span>
</div>
</template>
</el-alert>
</el-col>
<el-col v-if="submission.data && !isCE" :span="22">
<vxe-table align="center" :data="submission.data" stripe style="padding-top: 13px;">
<vxe-table-column field="sid" title="ID" width="213"></vxe-table-column>
<vxe-table-column
field="stime"
title="Submit time"
width="213"
></vxe-table-column>
<vxe-table-column field="pid" title="Problem ID" width="213">
<template v-slot="{ row }">
<a
:href="getProblemUri(row.pid)"
style="color: rgb(87, 163, 243)"
>{{ row.pid }}</a
>
</template>
</vxe-table-column>
<vxe-table-column field="status" title="Status" width="213">
<template v-slot="{ row }">
<span :class="getStatusColor(row.status)">{{
JUDGE_STATUS[row.status].name
}}</span>
</template>
</vxe-table-column>
<vxe-table-column
field="time"
title="Time"
width="213"
></vxe-table-column>
<vxe-table-column
field="memory"
title="Memory"
width="213"
></vxe-table-column>
</vxe-table>
</el-col>
<el-col :span="22">
<Highlight
:code="submission.code"
:language="submission.language"
:border-color="status.color"
></Highlight>
</el-col>
<el-col v-if="submission.can_unshare" :span="22">
<div id="share-btn">
<el-button type="primary" icon="el-icon-document-copy" size="large" @click="doCopy">Copy</el-button>
<el-button
v-if="submission.shared"
type="warning"
size="large"
icon="el-icon-circle-close"
@click="shareSubmission(false)"
>
Unshared
</el-button>
<el-button
v-else
type="primary"
size="large"
icon="el-icon-share"
@click="shareSubmission(true)"
>
Shared
</el-button>
</div>
</el-col>
</el-row>
</template>
<script>
import api from "@/common/api";
import { JUDGE_STATUS } from "@/common/constants";
import utils from "@/common/utils";
import Highlight from "@/components/common/Highlight";
import mMessage from '@/common/message'
export default {
name: "submissionDetails",
components: {
Highlight,
},
data() {
return {
submission: {
code:
"import java.util.Scanner;\n\
public 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\
}",
data: [{
sid: 1000,
stime: "2020-08-08 16:00:00",
pid: "1001",
status: 0,
time: "4ms",
memory: "3MB",
language:'Java',
author: "Himit_ZH",
err_info:'CE错误',
}],
can_unshare:true,
shared:true
},
isConcat: false,
loading: false,
JUDGE_STATUS:'',
};
},
mounted() {
this.getSubmission();
this.JUDGE_STATUS = Object.assign({}, JUDGE_STATUS);
},
methods: {
doCopy() {
this.$copyText(this.submission.code).then(function (e) {
mMessage.success('Code copied successfully');
}, function (e) {
mMessage.success('Code copy failed');
})
},
getProblemUri(pid) {
return "/problem/" + pid;
},
getStatusColor(status) {
return "el-tag el-tag--medium status-" + JUDGE_STATUS[status].color;
},
getbackgroudColor(status){
return "status-" + JUDGE_STATUS[status].color;
},
getSubmission() {
this.loading = true;
api.getSubmission(this.$route.params.id).then(
(res) => {
this.loading = false;
let data = res.data.data;
if (data.info && data.info.data && !this.isConcat) {
// score exist means the submission is OI problem submission
if (data.info.data[0].score !== undefined) {
this.isConcat = true;
const scoreColumn = {
title: this.$i18n.t("m.Score"),
align: "center",
key: "score",
};
this.columns.push(scoreColumn);
this.loadingTable = false;
}
if (this.isAdminRole) {
this.isConcat = true;
const adminColumn = [
{
title: this.$i18n.t("m.Real_Time"),
align: "center",
render: (h, params) => {
return h(
"span",
utils.submissionTimeFormat(params.row.real_time)
);
},
},
{
title: this.$i18n.t("m.Signal"),
align: "center",
key: "signal",
},
];
this.columns = this.columns.concat(adminColumn);
}
}
this.submission = data;
},
() => {
this.loading = false;
}
);
},
shareSubmission(shared) {
let data = { id: this.submission.data[0].sid, shared: shared };
api.updateSubmission(data).then(
(res) => {
this.getSubmission();
this.$success(this.$i18n.t("m.Succeeded"));
},
() => {}
);
},
},
computed: {
status() {
return {
type: JUDGE_STATUS[this.submission.data[0].status].type,
statusName: JUDGE_STATUS[this.submission.data[0].status].name,
color: JUDGE_STATUS[this.submission.data[0].status].rgb,
};
},
isCE() {
return this.submission.data[0].status === -2;
},
isAdminRole() {
return this.$store.getters.isAdminRole;
},
},
};
</script>
<style scoped>
#status .title {
font-size: 20px;
}
#status .content {
margin-top: 10px;
font-size: 14px;
}
#status .content span {
margin-right: 10px;
}
#status .span-row{
display:block;
float:left
}
#status .content pre {
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
}
#share-btn {
float: right;
margin-top: 5px;
margin-right: 10px;
}
.el-row--flex {
flex-wrap: wrap;
}
pre {
border: none;
background: none;
}
</style>

View File

@ -4,9 +4,21 @@
<el-card shadow> <el-card shadow>
<div slot="header"> <div slot="header">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :md="15" :lg="15"> <el-col :md="2" :lg="2">
<span class="panel-title hidden-xs-only" >Problem List</span> <span class="panel-title hidden-xs-only" >Status</span>
</el-col> </el-col>
<el-col :xs="16" :md="10" :lg="10">
<el-switch
style="display: block"
v-model="formFilter.myself"
active-color="#ff4949"
active-text="Mine"
:width="40"
inactive-text="All">
</el-switch>
</el-col>
<el-col :xs="8" :md="4" :lg="4"> <el-col :xs="8" :md="4" :lg="4">
<el-dropdown <el-dropdown
class="drop-menu" class="drop-menu"
@ -16,7 +28,7 @@
> >
<span class="el-dropdown-link"> <span class="el-dropdown-link">
{{formFilter.status === '' ? 'Status' : formFilter.status}} {{formFilter.status === '' ? 'Status' : formFilter.status}}
<i class="el-icon-arrow-down el-icon--right"></i> <i class="el-icon-caret-bottom"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="All">All</el-dropdown-item> <el-dropdown-item command="All">All</el-dropdown-item>
@ -26,24 +38,46 @@
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</el-col> </el-col>
<el-col :xs="16" :md="5" :lg="5">
<vxe-input v-model="formFilter.pid" placeholder="Enter keyword" type="search" size="medium" @search-click="filterByKeyword"></vxe-input> <el-col :xs="24" :md="4" :lg="4" class="search">
<vxe-input v-model="formFilter.pid" placeholder="Enter Problem ID" type="search" size="medium" @search-click="filterByKeyword"></vxe-input>
</el-col>
<el-col :xs="24" :md="4" :lg="4" class="search">
<vxe-input v-model="formFilter.pid" placeholder="Enter Author" type="search" size="medium" @search-click="filterByKeyword"></vxe-input>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<vxe-table <vxe-table
border="inner" border="inner"
stripe stripe
highlight-current-row
highlight-hover-row
align="center"
:row-class-name="tableRowClassName"
:data="submissions"> :data="submissions">
<vxe-table-column field="sid" title="ID" width="100"></vxe-table-column> <vxe-table-column field="sid" title="ID" width="150"></vxe-table-column>
<vxe-table-column field="stime" title="Submit time" width="300" type="html"></vxe-table-column> <vxe-table-column field="stime" title="Submit time" width="150"></vxe-table-column>
<vxe-table-column field="pid" title="Problem ID" width="150" type="html"></vxe-table-column> <vxe-table-column field="pid" title="Problem ID" width="150">
<vxe-table-column field="status" title="Status" width="250" type="html"></vxe-table-column> <template v-slot="{ row }">
<vxe-table-column field="time" title="Time" width="100"></vxe-table-column> <a :href="getProblemUri(row.pid)" style="color:rgb(87, 163, 243);">{{row.pid}}</a>
<vxe-table-column field="memory" title="Memory" width="100"></vxe-table-column> </template>
<vxe-table-column field="language" title="Language" width="100"></vxe-table-column> </vxe-table-column>
<vxe-table-column field="judger" title="Judger" width="100"></vxe-table-column> <vxe-table-column field="status" title="Status" width="150">
<vxe-table-column field="author" title="Author" width="100"></vxe-table-column> <template v-slot="{ row }">
<span :class="getStatusColor(row.status)">{{JUDGE_STATUS[row.status].name}}</span>
</template>
</vxe-table-column>
<vxe-table-column field="time" title="Time" width="150"></vxe-table-column>
<vxe-table-column field="memory" title="Memory" width="150"></vxe-table-column>
<vxe-table-column field="language" title="Language" width="150">
<template v-slot="{ row }">
<el-tooltip class="item" effect="dark" content="查看提交详情" placement="top">
<el-button type="text" @click="showSubmitDetail(row)">{{ row.language }}</el-button>
</el-tooltip>
</template>
</vxe-table-column>
<vxe-table-column field="judger" title="Judger" width="150"></vxe-table-column>
<vxe-table-column field="author" title="Author" width="150"></vxe-table-column>
</vxe-table> </vxe-table>
</el-card> </el-card>
<Pagination :total="total" :page-size="limit" @on-change="changeRoute" :current.sync="page"></Pagination> <Pagination :total="total" :page-size="limit" @on-change="changeRoute" :current.sync="page"></Pagination>
@ -58,11 +92,10 @@
import utils from '@/common/utils' import utils from '@/common/utils'
import time from '@/common/time' import time from '@/common/time'
import Pagination from '@/components/common/Pagination' import Pagination from '@/components/common/Pagination'
export default { export default {
name: 'submissionList', name: 'submissionList',
components: { components: {
Pagination Pagination,
}, },
data () { data () {
return { return {
@ -73,7 +106,42 @@
pid:'' pid:''
}, },
loadingTable: false, loadingTable: false,
submissions: [], submissions: [
{
sid:1000,
stime:'2020-08-08 16:00:00',
pid:'1001',
status:0,
time:'4ms',
memory:'3MB',
language:'C++',
judger:'192.168.1.1',
author:'Himit_ZH'
},
{
sid:1001,
stime:'2020-08-08 16:00:00',
pid:'1001',
status:0,
time:'4ms',
memory:'3MB',
language:'C++',
judger:'192.168.1.1',
author:'Himit_Z'
},
{
sid:1000,
stime:'2020-08-08 16:00:00',
pid:'1001',
status:0,
time:'4ms',
memory:'3MB',
language:'C++',
judger:'192.168.1.1',
author:'Himit_H'
}
],
total: 30, total: 30,
limit: 15, limit: 15,
page: 1, page: 1,
@ -193,6 +261,21 @@
}, () => { }, () => {
this.submissions[index].loading = false this.submissions[index].loading = false
}) })
},
showSubmitDetail (row) {
this.selectSubmitRow = row;
this.showDetails = true;
},
getProblemUri(pid){
return '/problem/'+pid;
},
getStatusColor(status){
return 'el-tag el-tag--medium status-'+JUDGE_STATUS[status].color;
},
tableRowClassName({row, rowIndex}){
if(row.author =='Himit_ZH'){
return 'own-submit-row';
}
} }
}, },
computed: { computed: {
@ -230,8 +313,11 @@
</script> </script>
<style scoped > <style scoped >
.ivu-btn-text {
color: #57a3f3; @media only screen and (max-width: 767px){
.search{
margin-top: 20px;
}
} }
.flex-container #main { .flex-container #main {
@ -245,4 +331,19 @@
flex: none; flex: none;
width: 210px; width: 210px;
} }
/deep/ .el-card__header {
border-bottom: 0px;
padding-bottom: 0px;
text-align: center;
}
/deep/ .el-button{
padding: 0;
}
/deep/ .el-dialog {
border-radius: 6px !important;
text-align: center;
}
/deep/ .el-switch{
padding-top: 6px;
}
</style> </style>