前端页面添加

This commit is contained in:
Himit_ZH 2020-11-22 23:28:13 +08:00
parent b31b8a9b3c
commit 98f0c2da2f
24 changed files with 2699 additions and 704 deletions

View File

@ -13,6 +13,7 @@
| 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 | | 2020-11-16 | 前端提交列表页,提交详情页,题目详情页,排行(ACM,OI)页,比赛列表页,个人主页,个人设置页 | Himit_ZH |
| 2020-11-22 | 前端比赛首页,比赛题目列表,比赛排行榜,比赛公告,首页布局调整 | Himit_ZH |
# 二、系统架构 # 二、系统架构

View File

@ -2,7 +2,7 @@
<div> <div>
<NavBar></NavBar> <NavBar></NavBar>
<div id="app"> <div id="app">
<transition name="fadeInUp" mode="out-in"> <transition name="el-zoom-in-bottom">
<router-view></router-view> <router-view></router-view>
</transition> </transition>
</div> </div>
@ -24,6 +24,7 @@ export default {
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
} }
body{ body{
background-color: #eee; background-color: #eee;
@ -35,7 +36,7 @@ export default {
code, kbd, pre, samp { code, kbd, pre, samp {
font-family: Consolas,Menlo,Courier,monospace; font-family: Consolas,Menlo,Courier,monospace;
} }
a { a {
text-decoration: none; text-decoration: none;
background-color: transparent; background-color: transparent;
@ -57,6 +58,27 @@ export default {
padding-bottom: 20px; padding-bottom: 20px;
line-height: 30px; line-height: 30px;
} }
.home-title{
color: #409EFF;
}
.oi-100,.first-ac{
background-color: #080;
color: #fff;
font-weight: 700;
}
.oi-between{
background-color: #2d8cf0;
color: #fff;
}
.ac{
background-color: #a9f5af;
color: #3c763d;
}
.oi-0,.wa{
color: #a94442;
background-color: #f2dede;
}
.status-green{ .status-green{
background-color: #19be6b!important; background-color: #19be6b!important;
color: #fff!important; color: #fff!important;
@ -83,6 +105,10 @@ export default {
.vxe-table{ .vxe-table{
color: #495060!important; color: #495060!important;
font-size: 12px!important; font-size: 12px!important;
font-weight: 500!important;
}
.vxe-table .vxe-body--column:not(.col--ellipsis), .vxe-table .vxe-footer--column:not(.col--ellipsis), .vxe-table .vxe-header--column:not(.col--ellipsis) {
padding: 9px 0!important;
} }
#nprogress .bar { #nprogress .bar {
background: #66B1FF !important; background: #66B1FF !important;
@ -99,10 +125,22 @@ export default {
padding: 0 4%; padding: 0 4%;
} }
} }
@media screen and (max-width: 768px) {
#app {
margin-top: 210px;
padding: 0 0;
}
.markdown-body img{
max-width: 100%;
}
}
@media screen and (max-width: 400px) { @media screen and (max-width: 400px) {
#app { #app {
margin-top: 270px; margin-top: 270px;
padding: 0 4%; padding: 0 0;
}
.markdown-body img{
width: 100%;
} }
} }
#problem-content .sample pre { #problem-content .sample pre {

View File

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

View File

@ -1,4 +1,11 @@
export const JUDGE_STATUS = { export const JUDGE_STATUS = {
'-10': {
name: 'Not submitted',
short: 'NS',
color: 'gray',
type: 'info',
rgb:'#909399'
},
'-3': { '-3': {
name: 'Presentation Error', name: 'Presentation Error',
short: 'PE', short: 'PE',
@ -156,7 +163,8 @@ export const CONTEST_TYPE_REVERSE = {
export const CONTEST_TYPE = { export const CONTEST_TYPE = {
PUBLIC: 'Public', PUBLIC: 'Public',
PRIVATE: 'Password Protected' PRIVATE: 'Password Protected',
PROTECT: 'Submit Protected'
} }
export const USER_TYPE = { export const USER_TYPE = {

View File

@ -1,7 +1,7 @@
import moment from 'moment' import moment from 'moment'
// convert utc time to localtime // convert utc time to localtime
function utcToLocal (utcDt, format = 'YYYY-M-D HH:mm:ss') { function utcToLocal (utcDt, format = 'YYYY-MM-DD HH:mm:ss') {
return moment.utc(utcDt).local().format(format) return moment.utc(utcDt).local().format(format)
} }
@ -16,13 +16,22 @@ function duration (startTime, endTime) {
return Math.abs(duration.asHours().toFixed(1)) + ' hours' return Math.abs(duration.asHours().toFixed(1)) + ' hours'
} }
function secondFormat (seconds) { function secondFormat (time) {
let m = moment.duration(seconds, 'seconds') let m = moment.duration(time, 'seconds')
return Math.floor(m.asHours()) + ':' + m.minutes() + ':' + m.seconds() let seconds = m.seconds()>=10?m.seconds():'0'+m.seconds();
let hours = Math.floor(m.asHours())>=10?Math.floor(m.asHours()):'0'+Math.floor(m.asHours());
let minutes = m.minutes()>=10?m.minutes():'0'+m.minutes();
return hours + ':' + minutes + ':' + seconds;
} }
function durationMs (startTime, endTime) { // 计算时间段的时间戳
let start = moment(startTime)
let end = moment(endTime)
return end.diff(start, 'seconds');
}
export default { export default {
utcToLocal: utcToLocal, utcToLocal: utcToLocal,
duration: duration, duration: duration,
secondFormat: secondFormat secondFormat: secondFormat,
durationMs:durationMs,
} }

View File

@ -1,7 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import storage from '@/common/storage' import storage from '@/common/storage'
import { STORAGE_KEY } from '@/common/constants' import { STORAGE_KEY } from '@/common/constants'
import ojAPI from '@/common/api'
function submissionMemoryFormat (memory) { function submissionMemoryFormat (memory) {
if (memory === undefined) return '--' if (memory === undefined) return '--'
@ -39,7 +38,7 @@ function breakLongWords (value, length = 16) {
re = new RegExp('(.{' + length + '})', 'g') re = new RegExp('(.{' + length + '})', 'g')
} else { } else {
// 中文字符 // 中文字符
re = new RegExp('(.{' + (length / 2 + 1) + '})', 'g') re = new RegExp('(.{' + (parseInt(length / 2) + 1) + '})', 'g')
} }
return value.replace(re, '$1\n') return value.replace(re, '$1\n')
} }

View File

@ -0,0 +1,241 @@
<template>
<el-card shadow :padding="10">
<div slot="header">
<span class="panel-title" v-if="isContest">{{ title }}</span>
<span v-else class="home-title panel-title">{{ title}}</span>
<span style="float: right">
<el-button
v-if="listVisible"
type="primary"
@click="init"
size="small"
icon="el-icon-refresh"
:loading="btnLoading"
>Refresh</el-button
>
<el-button v-else type="primary" icon="el-icon-back" @click="goBack" size="small"
>Back</el-button
>
</span>
</div>
<transition-group name="el-zoom-in-bottom">
<div
class="no-announcement"
v-if="!announcements.length"
key="no-announcement"
>
<p>No Announcements</p>
</div>
<template v-if="listVisible">
<ul class="announcements-container" key="list">
<li v-for="announcement in announcements" :key="announcement.title">
<div class="flex-container">
<div class="title">
<a class="entry" @click="goAnnouncement(announcement)">
{{ announcement.title }}</a
>
</div>
<div class="info">
<span class="date">
<i class="el-icon-edit"></i>
{{ announcement.create_time | localtime }}
</span>
<span class="creator">
<i class="el-icon-user"></i>
{{ announcement.created_by.username }}
</span>
</div>
</div>
</li>
</ul>
<Pagination
v-if="!isContest"
key="page"
:total="total"
:page-size="limit"
@on-change="getAnnouncementList"
>
</Pagination>
</template>
<template v-else>
<div
v-katex
v-html="announcement.content"
key="content"
class="content-container markdown-body"
></div>
</template>
</transition-group>
</el-card>
</template>
<script>
import api from "@/common/api";
import Pagination from "@/components/common/Pagination";
export default {
name: "Announcement",
components: {
Pagination,
},
data() {
return {
limit: 10,
total: 10,
btnLoading: false,
announcements: [{
"id": 6,
"created_by": {
"id": 1,
"username": "root",
"real_name": null
},
"title": "\u8d5e\u52a9\u672c\u7ad9\u7684\u670d\u52a1\u5668 O(\u2229_\u2229)O\u8c22\u8c22",
"content": "<p><img alt=\"IMG_0264.JPG\" src=\"/public/upload/4cd6928f19.jpg\" width=\"425\" height=\"346\" /><br /></p><p><br /></p><ul><li>\u8d5e\u52a9\u672c\u7ad9\u8d2d\u4e70\u66f4\u597d\u7684\u670d\u52a1\u5668\uff0c\u9632\u6b62\u90e8\u5206\u9898\u76ee\u548c\u7f16\u8bd1\u5668\u975e\u9884\u671f\u7684\u8d85\u65f6</li><li>\u8d5e\u52a9\u672c\u7ad9\u7684CDN</li></ul><p><span style=\"color: rgb(65, 140, 175);\">O(\u2229_\u2229)O\u8c22\u8c22</span></p>",
"create_time": "2019-06-06T11:37:14.950009Z",
"last_update_time": "2019-10-10T14:08:25.887019Z",
"visible": true
},
{
"id": 5,
"created_by": {
"id": 1,
"username": "root",
"real_name": null
},
"title": "\u672c\u7ad9\u5c06\u5728\u672a\u6765\u4e00\u5468\u5185\u968f\u65f6\u8fc1\u79fb\u90e8\u7f72\u673a\u5668\uff0c\u5982\u6709\u9700\u6c42\u8bf7\u63d0\u524d\u7533\u8bf7 [5.3 \u5df2\u7ecf\u5b8c\u6210]",
"content": "<p>RT</p><p><br /></p><p>Update:5.3 \u5df2\u7ecf\u5b8c\u6210</p>",
"create_time": "2019-04-28T02:29:40.726247Z",
"last_update_time": "2019-05-03T02:23:10.504187Z",
"visible": true
},],
announcement: "",
listVisible: true,
};
},
mounted() {
// this.init();
},
methods: {
init() {
if (this.isContest) {
this.getContestAnnouncementList();
} else {
this.getAnnouncementList();
}
},
getAnnouncementList(page = 1) {
this.btnLoading = true;
api.getAnnouncementList((page - 1) * this.limit, this.limit).then(
(res) => {
this.btnLoading = false;
this.announcements = res.data.data.results;
this.total = res.data.data.total;
},
() => {
this.btnLoading = false;
}
);
},
getContestAnnouncementList() {
this.btnLoading = true;
api.getContestAnnouncementList(this.$route.params.contestID).then(
(res) => {
this.btnLoading = false;
this.announcements = res.data.data;
},
() => {
this.btnLoading = false;
}
);
},
goAnnouncement(announcement) {
this.announcement = announcement;
this.listVisible = false;
},
goBack() {
this.listVisible = true;
this.announcement = "";
},
},
computed: {
title() {
if (this.listVisible) {
return this.isContest ? "Contest Announcements" : "Announcements";
} else {
return this.announcement.title;
}
},
isContest() {
return !!this.$route.params.contestID;
},
},
};
</script>
<style scoped>
.announcements-container {
margin-top: -10px;
margin-bottom: 10px;
}
.announcements-container li {
padding-top: 15px;
list-style: none;
padding-bottom: 15px;
margin-left: 20px;
margin-top:10px;
font-size: 16px;
border: 1px solid rgba(187, 187, 187, 0.5);
border-left: 2px solid #409EFF;
}
/* .announcements-container li:last-child {
border-bottom: none;
} */
.flex-container{
text-align: center;
}
.flex-container .info{
margin-top: 5px;
}
.flex-container .title .entry {
color: #495060;
font-style: oblique;
}
.flex-container .title a:hover {
color: #2d8cf0;
border-bottom: 1px solid #2d8cf0;
}
.creator {
width: 200px;
text-align: center;
}
.date {
width: 200px;
text-align: center;
margin-right: 5px;
}
.content-container {
padding: 0 20px 20px 20px;
}
.no-announcement {
text-align: center;
font-size: 16px;
}
.announcement-animate-enter-active {
animation: fadeIn 1s;
}
ul{
list-style-type:none;
padding-inline-start:0px;
}
</style>

View File

@ -10,13 +10,11 @@ import ContestList from "@/views/contest/ContestList.vue"
import Problem from "@/views/problem/Problem.vue" import Problem from "@/views/problem/Problem.vue"
import ACMRank from "@/views/rank/ACMRank.vue" import ACMRank from "@/views/rank/ACMRank.vue"
import OIRank from "@/views/rank/OIRank.vue" import OIRank from "@/views/rank/OIRank.vue"
import CountDown from "@/views/contest/test.vue" import ContestDetails from "@/views/contest/ContestDetails.vue"
import ContestProblemList from "@/views/contest/children/ContestProblemList.vue"
import ContestRank from "@/views/contest/children/ContestRank.vue"
import Announcements from "@/components/common/Announcements.vue"
const routes = [ const routes = [
{
path: '/count-down',
name: 'CountDown',
component: CountDown
},
{ {
path: '/', path: '/',
redirect: { redirect: {
@ -86,5 +84,43 @@ const routes = [
component: Logout, component: Logout,
meta: { requireAuth: true, title: 'Logout' } meta: { requireAuth: true, title: 'Logout' }
}, },
{
name: 'contest-details',
path: '/contest/:contestID/',
component:ContestDetails,
meta: {title: 'Contest Details'},
children: [
{
name: 'contest-submission-list',
path: 'submissions',
component: SubmissionList
},
{
name: 'contest-problem-list',
path: 'problems',
component: ContestProblemList
},
{
name: 'contest-problem-details',
path: 'problem/:problemID/',
component: Problem
},
{
name: 'contest-announcement-list',
path: 'announcements',
component: Announcements
},
{
name: 'contest-rank',
path: 'rank',
component: ContestRank
},
// {
// name: 'acm-helper',
// path: 'helper',
// component: ACMContestHelper
// }
]
},
] ]
export default routes export default routes

View File

@ -138,8 +138,8 @@ const actions = {
api.getContest(rootState.route.params.contestID).then((res) => { api.getContest(rootState.route.params.contestID).then((res) => {
resolve(res) resolve(res)
let contest = res.data.data let contest = res.data.data
commit(types.CHANGE_CONTEST, {contest: contest}) commit('changeContest', {contest: contest})
commit(types.NOW, {now: moment(contest.now)}) commit('now', {now: moment(contest.now)})
if (contest.contest_type === CONTEST_TYPE.PRIVATE) { if (contest.contest_type === CONTEST_TYPE.PRIVATE) {
dispatch('getContestAccess') dispatch('getContestAccess')
} }
@ -159,17 +159,17 @@ const actions = {
} }
return -1 return -1
}) })
commit(types.CHANGE_CONTEST_PROBLEMS, {contestProblems: res.data.data}) commit('changeContestProblems', {contestProblems: res.data.data})
resolve(res) resolve(res)
}, () => { }, () => {
commit(types.CHANGE_CONTEST_PROBLEMS, {contestProblems: []}) commit('changeContestProblems', {contestProblems: []})
}) })
}) })
}, },
getContestAccess ({commit, rootState}) { getContestAccess ({commit, rootState}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
api.getContestAccess(rootState.route.params.contestID).then(res => { api.getContestAccess(rootState.route.params.contestID).then(res => {
commit(types.CONTEST_ACCESS, {access: res.data.data.access}) commit('contestAccess', {access: res.data.data.access})
resolve(res) resolve(res)
}).catch() }).catch()
}) })

View File

@ -76,9 +76,9 @@ const rootActions = {
}, },
changeDomTitle ({commit, state}, payload) { changeDomTitle ({commit, state}, payload) {
if (payload && payload.title) { if (payload && payload.title) {
window.document.title = state.website.website_name_shortcut + ' | ' + payload.title window.document.title = 'HOJ' + ' - ' + payload.title
} else { } else {
window.document.title = state.website.website_name_shortcut + ' | ' + state.route.meta.title window.document.title = 'HOJ' + ' - ' + state.route.meta.title
} }
} }
} }

View File

@ -1,126 +1,111 @@
<template> <template>
<div> <div>
<el-card class="contest"> <el-card class="contest">
<div slot="header" class="clearfix title"> <div slot="header" class="clearfix title">
<el-link @click="goContest" :underline="false">比赛标题</el-link> <el-link @click="goContest" :underline="false">比赛标题</el-link>
</div> </div>
<el-carousel indicator-position="outside" height="200px" :interval='interval'> <el-carousel
<el-carousel-item v-for="(contest,index) in contests" :key="index"> indicator-position="outside"
<div class="contest-info"> height="200px"
:interval="interval"
>
<el-carousel-item v-for="(contest, index) in contests" :key="index">
<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"
{{contest.beginTime | localtime }} ><i class="fa fa-calendar"></i>
{{ 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"
{{contest.duration}} ><i class="fa fa-clock-o"></i>
{{ contest.duration }}
</el-button> </el-button>
<el-button type="warning" round size="mini"><i class="fa fa-trophy"></i> <el-button type="warning" round size="mini"
{{contest.type}} ><i class="fa fa-trophy"></i>
{{ contest.type }}
</el-button> </el-button>
</div> </div>
<div class="contest-description"> <div class="contest-description">
<blockquote v-html="contest.description"></blockquote> <blockquote v-html="contest.description"></blockquote>
</div> </div>
</div> </div>
</el-carousel-item> </el-carousel-item>
</el-carousel> </el-carousel>
</el-card> </el-card>
<el-row> <el-row :gutter="20">
<el-col :md="14" :sm="24"> <el-col :md="12" :sm="24">
<el-card class="box-card"> <Announcements></Announcements>
<div slot="header" class="clearfix">
<h2>Other OnlineJudge Contest</h2>
</div>
<el-table
:data="tableData"
height="400"
border
style="width: 100%;border-radius: 8px;">
<el-table-column
prop="oj"
label="OJ"
width="100">
</el-table-column>
<el-table-column
prop="title"
label="Title"
>
</el-table-column>
<el-table-column
prop="beginTime"
width="160"
label="Begin time">
</el-table-column>
<el-table-column
prop="endTime"
width="160"
label="End time">
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :md="10" :sm="24">
<div class="block">
<el-timeline>
<el-timeline-item timestamp="2018/4/12" placement="top" icon="el-icon-more" type="primary">
<el-card>
<h4>更新 Github 模板</h4>
<p>王小虎 提交于 2018/4/12 20:46</p>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2018/4/3" placement="top">
<el-card>
<h4>更新 Github 模板</h4>
<p>王小虎 提交于 2018/4/3 20:46</p>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2018/4/2" placement="top">
<el-card>
<h4>更新 Github 模板</h4>
<p>王小虎 提交于 2018/4/2 20:46</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-col> </el-col>
</el-row> <el-col :md="12" :sm="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span class="panel-title home-title"
>Other OnlineJudge Contest</span
>
</div>
<vxe-table
border="inner"
stripe
auto-resize
:data="otherContests">
<vxe-table-column field="oj" title="OJ" min-width="100"></vxe-table-column>
<vxe-table-column field="title" title="Title" min-width="200"></vxe-table-column>
<vxe-table-column field="beginTime" title="Begin Time" min-width="150">
<template v-slot="{ row }" >
<span>{{row.beginTime| localtime}}</span>
</template>
</vxe-table-column>
<vxe-table-column field="endTime" title="End Time" min-width="150">
<template v-slot="{ row }" >
<span>{{row.endTime| localtime}}</span>
</template>
</vxe-table-column>
</vxe-table>
</el-card>
</el-col>
</el-row>
</div> </div>
</template> </template>
<script> <script>
import time from "@/common/time"; import time from "@/common/time";
import Announcements from "@/components/common/Announcements.vue";
export default { export default {
name:"home", name: "home",
data() { components: {
return { Announcements,
interval:6000, },
tableData: [{ data() {
oj: 'Codeforces', return {
title: 'Codeforces Round #680 (Div. 1, based on VK Cup 2020-2021 - Final)', interval: 6000,
beginTime: '2020-11-08T05:00:00Z', otherContests: [
endTime:'2020-11-08T08:00:00Z', {
},], oj: "Codeforces",
contests: [ title:
{ "Codeforces Round #680 (Div. 1, based on VK Cup 2020-2021 - Final)",
beginTime:'2020-11-08T05:00:00Z', beginTime: "2020-11-08T05:00:00Z",
duration:'5hours', endTime: "2020-11-08T08:00:00Z",
type:"ACM", },
description:'<h1>描述</h1>', ],
} contests: [
], {
} beginTime: "2020-11-08T05:00:00Z",
}, duration: "5hours",
methods:{ type: "ACM",
goContest(){ description: "<h1>描述</h1>",
},
} ],
}, };
filters: { },
methods: {
goContest() {},
},
filters: {
localtime(value) { localtime(value) {
return time.utcToLocal(value); return time.utcToLocal(value);
}, },
}, },
} };
</script> </script>
<style scoped> <style scoped>
.el-carousel__item h3 { .el-carousel__item h3 {
@ -134,37 +119,37 @@ export default {
background-color: #fff; background-color: #fff;
} }
.el-col{ .el-col {
margin-top: 25px; margin-top: 25px;
} }
.contest{ .contest {
text-align: center; text-align: center;
} }
.clearfix:before, .clearfix:before,
.clearfix:after { .clearfix:after {
display: table; display: table;
content: ""; content: "";
} }
.clearfix:after { .clearfix:after {
clear: both clear: both;
} }
.contest .title .el-link { .contest .title .el-link {
font-size: 25px; font-size: 25px;
font-weight: 500; font-weight: 500;
color: #444; color: #444;
} }
.clearfix h2{ .clearfix h2 {
color: #409EFF; color: #409eff;
} }
.el-link.el-link--default:hover { .el-link.el-link--default:hover {
color: #409EFF; color: #409eff;
transition: all 0.28s ease; transition: all 0.28s ease;
} }
.contest .content-info { .contest .content-info {
padding: 0 70px 40px 70px; padding: 0 70px 40px 70px;
} }
.contest .contest-description{ .contest .contest-description {
margin-top: 25px; margin-top: 25px;
} }
</style> </style>

View File

@ -0,0 +1,342 @@
<template>
<div class="contest-body">
<el-row>
<el-col :xs="24" :md="24" :lg="24">
<el-card shadow>
<div class="contest-title">
<div slot="header">
<span class="panel-title">{{title}}</span>
</div>
</div>
<el-row>
<el-col :span="12" class="text-align:left">
<el-tooltip :content="CONTEST_TYPE_REVERSE[auth].tips" placement="top">
<el-tag
:type="CONTEST_TYPE_REVERSE[auth].color"
effect="plain">
{{CONTEST_TYPE_REVERSE[auth].name}}
</el-tag>
</el-tooltip>
</el-col>
<el-col :span="12" style="text-align:right">
<el-button size="small">
{{ rule_type }}
</el-button>
</el-col>
</el-row>
<div class="contest-time">
<el-row>
<el-col :xs="24" :md="12" class="left">
<p><i class="fa fa-hourglass-start" aria-hidden="true"></i> StartAt{{startTime | localtime}}</p>
</el-col>
<el-col :xs="24" :md="12" class="right">
<p><i class="fa fa-hourglass-end" aria-hidden="true"></i> EndAt{{endTime | localtime}}</p>
</el-col>
</el-row>
</div>
<div class="slider">
<el-slider
v-model="value"
:format-tooltip="formatTooltip"
:step="timeStep"
></el-slider>
</div>
<el-row>
<el-col :span="24" style="text-align:center">
<el-tag
effect="dark"
size="medium"
>
<i
class="fa fa-circle"
aria-hidden="true"
></i>
-11:16:52
<!-- {{ CONTEST_STATUS_REVERSE[status].name }} -->
</el-tag>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<div class="sub-menu" >
<!-- 判断是否需要密码验证 -->
<el-card v-if="passwordFormVisible" class="password-form-card" style="text-align:center">
<div slot="header">
<span class="panel-title">Password required</span>
</div>
<p class="password-form-tips">To enter the Private contest,please input the password!</p>
<el-input v-model="contestPassword" type="password"
placeholder="Enter the contest password"
@on-enter="checkPassword"/>
<el-button type="primary" @click="checkPassword" style="float:right;margin:5px">Enter</el-button>
</el-card>
<el-tabs v-else @tab-click="tabClick" v-model="route_name" :lazy="true">
<el-tab-pane name="contest-details">
<span slot="label"><i class="el-icon-s-home"></i>&nbsp;Overview</span>
<el-card class="box-card">
<div v-html="description" class="markdown-body"></div>
</el-card>
</el-tab-pane>
<el-tab-pane name="contest-problem-list">
<span slot="label"><i class="fa fa-list" aria-hidden="true"></i>&nbsp;Problems</span>
<transition name="el-zoom-in-bottom">
<router-view ></router-view>
</transition>
</el-tab-pane>
<el-tab-pane name="contest-submission-list">
<span slot="label"><i class="el-icon-menu"></i>&nbsp;Status</span>
<transition name="el-zoom-in-bottom">
<router-view></router-view>
</transition>
</el-tab-pane>
<el-tab-pane name="contest-rank">
<span slot="label"><i class="fa fa-bar-chart" aria-hidden="true"></i>&nbsp;Rank</span>
<transition name="el-zoom-in-bottom">
<router-view></router-view>
</transition>
</el-tab-pane>
<el-tab-pane name="contest-announcement-list">
<span slot="label"><i class="fa fa-bullhorn" aria-hidden="true"></i>&nbsp;Announcements</span>
<transition name="el-zoom-in-bottom">
<router-view></router-view>
</transition>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="fa fa-commenting" aria-hidden="true"></i>&nbsp;Comments</span>
<transition name="el-zoom-in-bottom">
<router-view></router-view>
</transition>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
import time from "@/common/time";
import moment from "moment";
import api from "@/common/api";
import { mapState, mapGetters, mapActions } from "vuex";
import {
CONTEST_STATUS_REVERSE,
CONTEST_STATUS,
CONTEST_TYPE_REVERSE,
} from "@/common/constants";
export default {
name: "",
data() {
return {
route_name: "contest-details",
value: 0,
duration: 0, //
nowDurationMs: 0, //
timer: null,
CONTEST_STATUS: CONTEST_STATUS,
CONTEST_STATUS_REVERSE: CONTEST_STATUS_REVERSE,
CONTEST_TYPE_REVERSE: CONTEST_TYPE_REVERSE,
btnLoading: false,
contestID: "",
contestPassword: "",
status: 0,
auth: "1",
contest_type: "Public",
title:"2020\u5e74 \u67e0\u6aac\u8336\u6b22\u4e50\u8d5b \u66a8 \u672c\u79d1\u751f\u521b\u65b0\u5b9e\u9a8c\u5ba4\u6708\u8d5b",
description: "<p>\u7ef4\u4ed6\u5165\u6211\u5fc3\uff0c\u8d5b\u8fc7\u6d77\u6d1b\u56e0\uff0c\u5728\u8fd9\u4e2a\u5bd2\u51b7\u7684\u51ac\u5929\uff0c\u4e3a\u4ec0\u4e48\u4e0d\u6765\u4e00\u676f\u51b0\u723d\u67e0\u6aac\u8336\u5462\uff1f</p><p><img src=\"https://pic1.zhimg.com/d8db4bb6d8dfc727b6f0a7f12d7367a0_r.jpg?source=1940ef5c\" alt=\"\" /></p><h1>\u6bd4\u8d5b\u5956\u54c1\u8bf4\u660e:</h1><p>\u672c\u6b21\u6bd4\u8d5b\u5177\u6709\u4e30\u539a\u7684\u5956\u54c1 (\u91cd\u8981\u7684\u4e0d\u662f\u4ef7\u503c\uff0c\u800c\u662f<b>\u6b22\u4e50</b>\u4e0d\u662f\u4e48\uff1f) \uff0c\u5177\u4f53\u89c4\u5219\u5982\u4e0b\uff1a</p><p>\u4e00\u3001\u6240\u6709\u53c2\u8d5b\u9009\u624b\u5747\u83b7\u5f97<b>\u53c2\u8d5b\u7eaa\u5ff5\u5956</b>\u2014\u2014<b>\u9ea6\u65af\u5a01\u5c14\u5496\u5561\u4e00\u5305</b></p><p>\u4e8c\u3001\u672c\u6b21\u6bd4\u8d5b\u6392\u540d<b>\u7b2c\u4e00\u3001\u7b2c\u5341</b>\u7684\u9009\u624b\u5404\u83b7\u5f97LZH\u5b66\u957f\u8d5e\u52a9\u7684<b>\u7ef4\u4ed6\u67e0\u6aac\u8336\u4e09\u74f6</b></p><p><span style=\"color: rgb(51, 51, 51);\">\u4e09\u3001\u672c\u6b21\u6bd4\u8d5b\u6392\u540d<b>\u7b2c\u4e8c\u3001\u7b2c\u5341\u4e00</b>\u7684\u9009\u624b\u5404\u83b7\u5f97LZH\u5b66\u957f\u8d5e\u52a9\u7684<b>\u798f\u5efa\u9ad8\u7aef\u8336\u53f6\u4e09\u888b</b></span><br /></p><p>\u56db\u3001\u672c\u6b21\u6bd4\u8d5b<b>\u6700\u540e\u4e00\u4e2aAC</b>\u7684\u540c\u5b66\u5c06\u83b7\u5f97\u987d\u5f3a\u62fc\u640f\u5956\uff0c\u5956\u54c1\u795e\u79d8\u7684\u5f88\uff08ZXF\u8d5e\u52a9\uff09\uff0c<b>\u73b0\u573a\u63ed\u6653</b>\uff0c\u594b\u6597\u5427baby\u4eec.</p><p>\u4e94\u3001\u672c\u6b21\u6bd4\u8d5b\u83b7\u5f97<b>\u7b2c\u4e00\u540d</b>\u7684\u9009\u624b\u5c06\u83b7\u5f97ZYB\u5b66\u957f\u8d5e\u52a9\u7684<b>\u9ad8\u7aef\u8f6f\u76ae\u672c\u4e00\u4e2a</b></p><p><span style=\"color: rgb(51, 51, 51);\">\u516d\u3001\u672c\u6b21\u6bd4\u8d5b\u83b7\u5f97<b>\u7b2c\u4e8c\u3001\u4e09\u3001\u56db\u3001\u4e94\u540d</b>\u7684\u9009\u624b\u5c06\u6bcf\u4eba\u83b7\u5f97LZH\u5b66\u957f\u8d5e\u52a9\u7684<b>\u8f6f\u76ae\u672c\u4e00\u4e2a</b></span><br /></p><p><span style=\"color: rgb(51, 51, 51);\"><span style=\"color: rgb(51, 51, 51);\">\u4e03\u3001\u672c\u6b21\u6bd4\u8d5b\u83b7\u5f97</span><span style=\"color: rgb(51, 51, 51);\"><b>\u7b2c\u516d\u3001\u4e03\u3001\u516b\u3001\u4e5d\u540d</b></span><span style=\"color: rgb(51, 51, 51);\">\u7684\u9009\u624b\u5c06\u6bcf\u4eba\u83b7\u5f97CGX\u8d5e\u52a9\u7684<b>\u53cc\u6c47\u8089\u5757\u738b\u7279\u7ea7\u706b\u817f\u80a0</b></span><span style=\"color: rgb(51, 51, 51);\"><b>\u4e00\u6839</b></span><br /></span></p><p>\u516b\u3001\u6bd4\u8d5b\u7ed3\u675f\u540e\uff0c<b>\u968f\u673a\u62bd\u53d6\u4e09\u540d\u540c\u5b66</b>\u9001<b>\u725b\u5ba2\u886c\u886b</b>\uff0c\u6ee1\u8db3\u4f60\u6c38\u8fdc\u62bd\u4e0d\u5230\u886c\u886b\u7684\u975e\u914b\u7684\u613f\u671b\uff01</p><p><b><u>\u4ee5\u4e0a\u89c4\u5219\u6700\u7ec8\u89e3\u91ca\u6743\u5f52\u521b\u65b0\u5b9e\u9a8c\u5ba4\u6240\u6709</u></b>\u3002</p><h1><font>\u7f5a\u65f6\u89c4\u5219\u548c\u8fd4\u56de\u7ed3\u679c\u89e3\u91ca:</font></h1><p><font>1.\u63d0\u4ea4\u663e\u793aAccepted\u5373\u4e3a\u901a\u8fc7,\u82e5\u663e\u793a\u5176\u4ed6\u7ed3\u679c\u5373\u8868\u793a\u9519\u8bef.\u9519\u8bef\u4e00\u6b21\u603b\u7f5a\u65f6+20\u5206\u949f,\u8bf7\u8c28\u614e\u63d0\u4ea4\uff01</font></p><p><font>2.\u5173\u4e8e\u8fd4\u56de\u7ed3\u679c\u7684\u89e3\u91ca:</font></p><p><font>Pending & Juding : You solution will be judged soon, please wait for result</font></p><p><font>Compile Error : Failed to compile your source code. Click on the link to see compiler&#039;s output.</font></p><p><font>Accepted : Congratulations. Your solution is correct.</font></p><p><font>Wrong Answer : Your program&#039;s output doesn&#039;t match judger&#039;s answer.</font></p><p><font>Runtime Error : Your program terminated abnormally. Possible reasons are: segment fault, divided by zero or exited with code other than 0.OrThe memory your program actually used has exceeded limit.</font></p><p><font>Time Limit Exceeded : The CPU time your program used has exceeded limit. Java has a triple time limit.</font></p><p><font>Memory Limit Exceeded : The memory your program actually used has exceeded limit.</font></p><p><font>System Error : Oops, something has gone wrong with the judger. Please report this to administrator.</font></p><h1><font>\u6bd4\u8d5b\u8981\u6c42:</font></h1><p><font>\u6bd4\u8d5b\u8fc7\u7a0b\u4e2d<b>\u4e0d\u8bb8\u8ba8\u8bba</b>\u4e0d\u8bb8\u4e0a\u5916\u7f51,\u6709\u95ee\u9898\u53ef\u4ee5\u4e3e\u624b!</font></p><p><font><b>\u9898\u76ee\u96be\u5ea6\u4e0e\u9898\u76ee\u987a\u5e8f\u65e0\u5173,\u8bf7\u5408\u7406\u5b89\u6392\u505a\u9898\u65f6\u95f4!</b></font></p>",
real_time_rank: true,
rule_type: "ACM",
startTime: "2018-04-17T11:16:52Z",
endTime: "2018-04-22T11:16:36Z",
now: "2020-11-17T15:16:22.013824Z",
};
},
mounted() {
this.contestID = this.$route.params.contestID;
this.route_name = this.$route.name;
if(this.route_name =='contest-problem-details'){ //
this.route_name = 'contest-problem-list'
}
// this.$store.dispatch("getContest").then((res) => {
// this.changeDomTitle({ title: res.data.data.title });
// let data = res.data.data;
// let endTime = moment(data.end_time);
// if (endTime.isAfter(moment(data.now))) {
// this.timer = setInterval(() => {
// this.$store.commit("nowAdd1s");
// }, 1000);
// }
// });
let nowTime = new Date().getTime(); //
this.duration = time.durationMs(this.startTime, this.endTime);
this.nowDurationMs = time.durationMs(this.startTime, nowTime);
if (this.nowDurationMs >= this.duration) {
//
this.nowDurationMs = this.duration;
this.value = 100;
} else {
this.timer = setInterval(() => {
// let nowTime = new Date().getTime(); //
// this.nowDurationMs = time.durationMs(this.startTime, nowTime); //
if (this.nowDurationMs >= 0) {
//
this.value = (this.nowDurationMs / this.duration) * 100; //
}
if (this.nowDurationMs >= this.duration) {
this.nowDurationMs = this.duration;
clearInterval(this.timer);
return;
}
this.nowDurationMs += 1;
}, 1000);
}
},
methods: {
...mapActions(["changeDomTitle"]),
formatTooltip(val) {
if (this.nowDurationMs <= 0) {
//
return "00:00:00";
}
return time.secondFormat(this.nowDurationMs); //
},
checkPassword() {
if (this.contestPassword === "") {
this.$error("Password can't be empty");
return;
}
this.btnLoading = true;
api.checkContestPassword(this.contestID, this.contestPassword).then(
(res) => {
this.$success("Succeeded");
this.$store.commit(types.CONTEST_ACCESS, { access: true });
this.btnLoading = false;
},
(res) => {
this.btnLoading = false;
}
);
},
tabClick(tab){
let name = tab.name;
if(name !==this.$route.name){
this.$router.push({name: name});
}
},
},
computed: {
timeStep() {
//
return 100 / this.duration;
},
...mapState({
showMenu: (state) => state.contest.itemVisible.menu,
contest: (state) => state.contest.contest,
contest_table: (state) => [state.contest.contest],
// now: (state) => state.contest.now,
}),
...mapGetters([
"contestMenuDisabled",
"contestRuleType",
"contestStatus",
"countdown",
"isContestAdmin",
"OIContestRealTimePermission",
"passwordFormVisible",
]),
countdownColor() {
if (this.contestStatus) {
return CONTEST_STATUS_REVERSE[this.contestStatus].color;
}
},
showAdminHelper() {
return this.isContestAdmin && this.contestRuleType === "ACM";
},
},
watch: {
$route(newVal) {
this.route_name = newVal.name;
this.contestID = newVal.params.contestID;
this.changeDomTitle({ title: this.contest.title });
},
},
filters: {
localtime(value) {
return time.utcToLocal(value);
},
},
beforeDestroy() {
clearInterval(this.timer);
this.$store.commit("clearContest");
},
};
</script>
<style scoped>
@media screen and (min-width: 768px) {
.contest-body {
padding: 0 8%;
}
.contest-time .left {
text-align: left;
}
.contest-time .right {
text-align: right;
}
.password-form-card{
width: 400px;
margin: 0 auto;
}
}
@media screen and (max-width: 768px) {
.contest-time .left,
.contest-time .right {
text-align: center;
}
}
/deep/.el-slider__button {
width: 20px !important;
height: 20px !important;
background-color: #409eff !important;
}
/deep/.el-slider__button-wrapper {
z-index: 500;
}
/deep/.el-slider__bar {
height: 10px !important;
background-color: #09be24 !important;
}
/deep/ .el-card__header {
border-bottom: 0px;
padding-bottom: 0px;
}
/deep/.el-tabs__nav-wrap{
background: #fff;
border-radius: 3px;
}
/deep/.el-tabs--top .el-tabs__item.is-top:nth-child(2){
padding-left: 20px;
}
.contest-title{
text-align: center;
}
.contest-time {
width: 100%;
font-size: 16px;
}
.el-tag--dark {
border-color: #fff;
}
.el-tag {
color: rgb(25, 190, 107);
background: #fff;
border: 1px solid #e9eaec;
font-size: 18px;
}
.sub-menu {
margin-top: 15px;
}
.password-form-tips{
text-align: center;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,624 @@
<template>
<el-card shadow>
<div slot="header"><span class="panel-title">Contest Rank</span>
<span style="float:right;font-size: 20px;">
<el-popover trigger="hover" placement="left-start">
<i class="el-icon-s-tools" slot="reference"></i>
<div id="switches">
<p>
<span>Chart</span>
<el-switch v-model="showChart"></el-switch>
</p>
<p>
<span>Table</span>
<el-switch v-model="showTable"></el-switch>
</p>
<p>
<span>Auto Refresh(10s)</span>
<el-switch :disabled="refreshDisabled" @on-change="handleAutoRefresh"></el-switch>
</p>
<template v-if="isContestAdmin">
<p>
<span>Force Update</span>
<el-switch :disabled="refreshDisabled" v-model="forceUpdate"></el-switch>
</p>
</template>
<template>
<el-button type="primary" size="small" @click="downloadRankCSV">Download as CSV</el-button>
</template>
</div>
</el-popover>
</span>
</div>
<div v-show="showChart" class="echarts">
<ECharts :options="options" ref="chart" :autoresize="true" ></ECharts>
</div>
<div v-show="showTable">
<vxe-table
round
border
auto-resize
size="mini"
align="center"
:data="dataRank"
:cell-class-name="cellClassName"
:seq-config="{startIndex: (this.page - 1) * this.limit}"
>
<vxe-table-column field="id" type="seq" min-width="50" fixed="left"></vxe-table-column>
<vxe-table-column field="username" min-width="150" title="User" >
<template v-slot="{ row }">
<span><a @click="getUserHomeByUsername(row.user.username)" style="color:rgb(87, 163, 243);">{{row.user.username}}</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column field="rating" title="AC / Total" min-width="80">
<template v-slot="{ row }">
<span>{{row.ac}} / <a @click="getUserTotalSubmit(row.user.username)" style="color:rgb(87, 163, 243);">{{row.total}}</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column field="total_time" title="TotalTime" min-width="80">
<template v-slot="{ row }">
<span>{{parseTotalTime(row.total_time)}}</span>
</template>
</vxe-table-column>
<vxe-table-column min-width="80" v-for="problem in problems" :key="problem.id">
<template v-slot:header>
<span><a @click="getContestProblemById(problem.id)">{{problem.id}}</a></span>
</template>
<template v-slot="{ row }">
<span v-if="row.submission_info[problem.id].is_ac">{{ row.submission_info[problem.id].ac_time }}<br></span>
<span v-if="row.submission_info[problem.id].error_number!=0">(-{{row.submission_info[problem.id].error_number}})</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
<Pagination :total="total"
:page-size.sync="limit"
:current.sync="page"
@on-change="getContestRankData"
@on-page-size-change="getContestRankData(1)"
show-sizer></Pagination>
</el-card>
</template>
<script>
import moment from 'moment'
import { mapActions } from 'vuex'
import Pagination from '@/components/common/Pagination'
import time from '@/common/time'
import utils from '@/common/utils'
import ContestRankMixin from './contestRankMixin'
export default {
name: 'acm-contest-rank',
mixins: [ContestRankMixin],
components: {
Pagination
},
data () {
return {
refreshDisabled:true,
isContestAdmin:true,
showChart:true,
showTable:true,
contest:{
start_time: "2018-04-17T11:16:52Z",
end_time: "2018-04-22T11:16:36Z",
now: "2020-11-17T15:16:22.013824Z",
},
total: 0,
page: 1,
limit:10,
contestID: '',
dataRank: [],
problems:[],
options: {
title: {
text: 'Top 10 Teams',
left: 'center',
top:0
},
dataZoom: [
{
type: 'inside',
filterMode: 'none',
xAxisIndex: [0],
start: 0,
end: 100
}
],
toolbox: {
show: true,
feature: {
saveAsImage: {show: true, title: 'save as image'}
},
right: '0'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
axis: 'x'
}
},
legend: {
orient: 'horizontal',
x:'center',
top:'8%',
right: 0,
data: [],
formatter: (value) => {
return utils.breakLongWords(value, 16)
},
textStyle: {
fontSize: 12
}
},
grid: {
x: 80,
x2: 100,
left: '5%', //canvas
top: '25%',
right: '5%',
bottom: '10%'
},
xAxis: [{
type: 'time',
splitLine: false,
axisPointer: {
show: true,
snap: true
}
}],
yAxis: [
{
type: 'category',
boundaryGap: false,
data: [0]
}],
series: []
}
}
},
mounted () {
this.contestID = this.$route.params.contestID
let dataRank=[
{
user:{
username:'Himit_ZH',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1000,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3600,error_number:0,is_first_ac:true,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:4500,error_number:0,is_first_ac:true,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user1',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:5555,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:6666,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'E':{ac_time:7000,error_number:0,is_first_ac:true,is_ac:true},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user2',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user2',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user3',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user4',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user5',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user6',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user7',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user6',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user7',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'我是你爸爸',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
{
user:{
username:'user9',
id:'id',
},
ac:9,
total:9,
total_time:10000,
submission_info:{
'A':{ac_time:1500,error_number:3,is_first_ac:false,is_ac:true},
'B':{ac_time:3800,error_number:0,is_first_ac:false,is_ac:true},
'C':{ac_time:0,error_number:1,is_first_ac:false,is_ac:false},
'D':{ac_time:8000,error_number:0,is_first_ac:false,is_ac:true},
'E':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'F':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'G':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'H':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'I':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
'J':{ac_time:0,error_number:0,is_first_ac:false,is_ac:false},
}
},
];
this.applyToTable(dataRank);
this.applyToChart(dataRank);
this.problems =[
{id:'A'},
{id:'B'},
{id:'C'},
{id:'D'},
{id:'E'},
{id:'F'},
{id:'G'},
{id:'H'},
{id:'I'},
{id:'J'}
];
this.addChartCategory(this.problems)
// this.getContestRankData(1)
// if (this.contestProblems.length === 0) {
// this.getContestProblems().then((res) => {
// this.addChartCategory(res.data.data)
// })
// } else {
// this.addChartCategory(this.contestProblems)
// }
},
methods: {
...mapActions(['getContestProblems']),
getUserTotalSubmit(username){
this.$router.push({
name: 'contest-submission-list',
query: {username: username}
})
},
getUserHomeByUsername(username){
this.$router.push(
{
name: 'user-home',
query: {username:username}
})
},
getContestProblemById(pid){
this.$router.push({
name: 'contest-problem-details',
params: {
contestID: this.contestID,
problemID: pid
}
})
},
cellClassName ({ row, rowIndex, column, columnIndex }) {
if (column.property !== 'id'&&column.property !== 'rating'&&column.property !== 'total_time'&&column.property !=='username') {
return row.cellClassName[[this.problems[columnIndex-4].id]]
}
},
applyToTable (dataRank) {
// // deepcopy
// let dataRank = JSON.parse(JSON.stringify(data))
dataRank.forEach((rank, i) => {
let info = rank.submission_info
let cellClass = {}
Object.keys(info).forEach(problemID => {
dataRank[i][problemID] = info[problemID]
dataRank[i][problemID].ac_time = time.secondFormat(dataRank[i][problemID].ac_time)
let status = info[problemID]
if (status.is_first_ac) {
cellClass[problemID] = 'first-ac'
} else if (status.is_ac) {
cellClass[problemID] = 'ac'
} else if(status.error_number!=0){
cellClass[problemID] = 'wa'
}
})
dataRank[i].cellClassName = cellClass
})
this.dataRank = dataRank
},
addChartCategory (contestProblems) {
let category = []
for (let i = 0; i <= contestProblems.length; ++i) {
category.push(i)
}
this.options.yAxis[0].data = category
},
applyToChart (rankData) {
let [users, seriesData] = [[], []]
rankData.forEach(rank => {
users.push(rank.user.username)
let info = rank.submission_info
// AC
let timeData = []
Object.keys(info).forEach(problemID => {
if (info[problemID].is_ac) {
timeData.push(info[problemID].ac_time)
}
})
timeData.sort((a, b) => {
return a - b
})
let data = []
data.push([this.contest.start_time, 0])
// index here can be regarded as stacked accepted number count.
for (let [index, value] of timeData.entries()) {
let realTime = moment(this.contest.start_time).add(value, 'seconds').format()
data.push([realTime, index + 1])
}
seriesData.push({
name: rank.user.username,
type: 'line',
data
})
})
this.options.legend.data = users
this.options.series = seriesData
},
parseTotalTime (totalTime) {
return time.secondFormat(totalTime)
},
downloadRankCSV () {
utils.downloadFile(`contest_rank?download_csv=1&contest_id=${this.$route.params.contestID}&force_refrash=${this.forceUpdate ? '1' : '0'}`)
}
}
}
</script>
<style scoped>
.echarts {
margin: 20px auto;
height: 400px;
width: 100%;
}
/deep/.el-card__body {
padding: 20px!important;
padding-top:0px!important;
}
.screen-full {
margin-right: 8px;
}
#switches p{
margin-top: 5px;
}
#switches p:first-child{
margin-top: 0;
}
#switches p span {
margin-left: 8px;
margin-right: 4px;
}
.vxe-cell p,.vxe-cell span{
margin: 0;
padding: 0;
}
/deep/.vxe-table .vxe-body--column{
line-height: 20px!important;
padding: 0!important;
}
@media screen and (max-width: 768px) {
/deep/.el-card__body {
padding: 0!important;
}
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<div class="problem-list">
<vxe-table v-if="contestRuleType == 'ACM' || OIContestRealTimePermission"
border="inner"
stripe
auto-resize
:data="problems"
@cell-click="goContestProblem">
<vxe-table-column field="status" title="" width="50">
<template v-slot="{ row }">
<el-tooltip :content="JUDGE_STATUS[row.status].name" placement="top">
<i class="el-icon-check" :style="getIconColor(row.status)" v-if="row.status==0" ></i>
<i class="el-icon-minus" :style="getIconColor(row.status)" v-else-if="row.status!=-10" ></i>
</el-tooltip>
</template>
</vxe-table-column>
<vxe-table-column field="id" width="80" title="#"></vxe-table-column>
<vxe-table-column field="title" title="Title" min-width="350"></vxe-table-column>
<vxe-table-column field="ac" title="AC" min-width="80"></vxe-table-column>
<vxe-table-column field="total" title="Total" min-width="80"></vxe-table-column>
<vxe-table-column field="ACRating" title="AC Rate" min-width="80"></vxe-table-column>
</vxe-table>
<!-- <Table v-else
:data="problems"
:columns="OITableColumns"
@on-row-click="goContestProblem"
no-data-text="$t('m.No_Problems')"></Table> -->
</div>
</template>
<script>
import {mapState, mapGetters} from 'vuex'
import utils from '@/common/utils'
import {JUDGE_STATUS} from "@/common/constants"
export default {
name: 'ContestProblemList',
data () {
return {
contestRuleType:"ACM",
JUDGE_STATUS:JUDGE_STATUS,
problems:[
{status:0,id:'A',title:'测试题目AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',ac:766,total:1000,ACRating:"76.6%"},
{status:1,id:'B',title:'测试题目AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',ac:766,total:1000,ACRating:"76.6%"}
]
// OITableColumns: [
// {
// title: '#',
// key: '_id',
// width: 150
// },
// {
// title: this.$i18n.t('m.Title'),
// key: 'title'
// }
// ]
}
},
mounted () {
// this.getContestProblems()
},
methods: {
getContestProblems () {
this.$store.dispatch('getContestProblems').then(res => {
if (this.isAuthenticated) {
if (this.contestRuleType === 'ACM') {
this.addStatusColumn(this.ACMTableColumns, res.data.data)
} else if (this.OIContestRealTimePermission) {
this.addStatusColumn(this.ACMTableColumns, res.data.data)
}
}
})
},
goContestProblem (event) {
console.log(event.row)
this.$router.push({
name: 'contest-problem-details',
params: {
contestID: this.$route.params.contestID,
problemID: event.row.id
}
})
},
getACRate (ACCount, TotalCount) {
return utils.getACRate(ACCount, TotalCount)
},
getIconColor(status){
return "font-weight: 600;font-size: 16px;color:"+JUDGE_STATUS[status].rgb;
}
},
computed: {
// ...mapState({
// problems: state => state.contest.contestProblems
// }),
// ...mapGetters(['isAuthenticated', 'contestRuleType', 'OIContestRealTimePermission'])
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,41 @@
<template>
<div>
<component :is="currentView"></component>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import ACMContestRank from './ACMContestRank.vue'
import OIContestRank from './OIContestRank.vue'
const NullComponent = {
name: 'null-component',
template: '<div></div>'
}
export default {
name: 'contest-rank',
components: {
ACMContestRank,
OIContestRank,
NullComponent
},
computed: {
...mapGetters(['contestRuleType']),
currentView () {
return 'ACMContestRank'
// if (this.contestRuleType === null) {
// return 'NullComponent'
// }
// return this.contestRuleType === 'ACM' ? 'ACMContestRank' : 'OIContestRank'
}
},
beforeRouteLeave (to, from, next) {
this.$store.commit("changeContestItemVisible", {menu: true})
next()
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,485 @@
<template>
<el-card shadow>
<div slot="header"><span class="panel-title">Contest Rank</span>
<span style="float:right;font-size: 20px;">
<el-popover trigger="hover" placement="left-start">
<i class="el-icon-s-tools" slot="reference"></i>
<div id="switches">
<p>
<span>Chart</span>
<el-switch v-model="showChart"></el-switch>
</p>
<p>
<span>Table</span>
<el-switch v-model="showTable"></el-switch>
</p>
<p>
<span>Auto Refresh(10s)</span>
<el-switch :disabled="refreshDisabled" @on-change="handleAutoRefresh"></el-switch>
</p>
<template v-if="isContestAdmin">
<p>
<span>Force Update</span>
<el-switch :disabled="refreshDisabled" v-model="forceUpdate"></el-switch>
</p>
</template>
<template>
<el-button type="primary" size="small" @click="downloadRankCSV">Download as CSV</el-button>
</template>
</div>
</el-popover>
</span>
</div>
<div v-show="showChart" class="echarts">
<ECharts :options="options" ref="chart" :autoresize="true"></ECharts>
</div>
<div v-show="showTable">
<vxe-table
round
border
auto-resize
size="small"
align="center"
:data="dataRank"
:cell-class-name="cellClassName"
:seq-config="{startIndex: (this.page - 1) * this.limit}"
>
<vxe-table-column field="id" type="seq" min-width="50" fixed="left"></vxe-table-column>
<vxe-table-column field="username" min-width="150" title="User" >
<template v-slot="{ row }">
<span><a @click="getUserHomeByUsername(row.user.username)" style="color:rgb(87, 163, 243);">{{row.user.username}}</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column field="total_score" title="TotalScore" min-width="80">
<template v-slot="{ row }">
<span><a @click="getUserTotalSubmit(row.user.username)" style="color:rgb(87, 163, 243);">{{row.total_score}}</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column min-width="80" v-for="problem in problems" :key="problem.id">
<template v-slot:header>
<span><a @click="getContestProblemById(problem.id)" class="emphasis">{{problem.id}}</a></span>
</template>
<template v-slot="{ row }">
<span v-if="row.submission_info[problem.id]!=-1">{{row.submission_info[problem.id]}}</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
<Pagination :total="total"
:page-size.sync="limit"
:current.sync="page"
@on-change="getContestRankData"
@on-page-size-change="getContestRankData(1)"
show-sizer></Pagination>
</el-card>
</template>
<script>
import { mapActions } from 'vuex'
import Pagination from '@/components/common/Pagination'
import ContestRankMixin from './contestRankMixin'
import utils from '@/common/utils'
export default {
name: 'acm-contest-rank',
components: {
Pagination
},
mixins: [ContestRankMixin],
data () {
return {
refreshDisabled:true,
isContestAdmin:true,
showChart:true,
showTable:true,
total: 0,
page: 1,
limit:10,
contestID: '',
dataRank: [],
problems:[],
options: {
title: {
text: 'Top 10 Teams',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
dataView: {show: true, readOnly: true},
magicType: {show: true, type: ['line', 'bar']},
saveAsImage: {show: true}
},
right: '10%',
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',
}
],
grid: {
left:'11%'
},
series: [
{
name: 'Score',
type: 'bar',
barMaxWidth: '80',
data: [0],
markPoint: {
data: [
{type: 'max', name: 'max'}
]
}
}
]
}
}
},
mounted () {
this.contestID = this.$route.params.contestID
// this.getContestRankData(1)
// if (this.contestProblems.length === 0) {
// this.getContestProblems().then((res) => {
// this.addTableColumns(res.data.data)
// })
// } else {
// this.addTableColumns(this.contestProblems)
// }
let dataRank=[{
user:{
username:'user1',
id:'id',
},
total_score:9999,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user2',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user3',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user4',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user5',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user6',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user7',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user8',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user9',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
{
user:{
username:'user10',
id:'id',
},
total_score:430,
submission_info:{
'A':100,
'B':-1,
'C':100,
'D':60,
'E':70,
'F':100,
'G':0,
'H':-1,
'I':-1
}
},
];
this.applyToTable(dataRank);
this.applyToChart(dataRank);
this.problems =[
{id:'A'},
{id:'B'},
{id:'C'},
{id:'D'},
{id:'E'},
{id:'F'},
{id:'G'},
{id:'H'},
{id:'I'},
];
},
methods: {
...mapActions(['getContestProblems']),
cellClassName ({ row, rowIndex, column, columnIndex }) {
if (column.property !== 'id'&&column.property !== 'total_score'&&column.property !=='username') {
return row.cellClassName[[this.problems[columnIndex-3].id]]
}
},
getUserTotalSubmit(username){
this.$router.push({
name: 'contest-submission-list',
query: {username: username}
})
},
getUserHomeByUsername(username){
this.$router.push(
{
name: 'user-home',
query: {username:username}
})
},
getContestProblemById(pid){
this.$router.push({
name: 'contest-problem-details',
params: {
contestID: this.contestID,
problemID: pid
}
})
},
applyToChart (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
},
applyToTable (dataRank) {
// let dataRank = JSON.parse(JSON.stringify(data))
dataRank.forEach((rank, i) => {
let info = rank.submission_info
let cellClass = {}
Object.keys(info).forEach(problemID => {
dataRank[i][problemID] = info[problemID]
let score = info[problemID]
if(score==0){
cellClass[problemID] = 'oi-0';
}else if(score>0&&score<100){
cellClass[problemID] = 'oi-between';
}else if(score==100){
cellClass[problemID] = 'oi-100';
}
})
dataRank[i].cellClassName = cellClass
})
this.dataRank = dataRank
},
downloadRankCSV () {
utils.downloadFile(`contest_rank?download_csv=1&contest_id=${this.$route.params.contestID}&force_refrash=${this.forceUpdate ? '1' : '0'}`)
}
}
}
</script>
<style scoped>
.echarts {
margin: 20px auto;
height: 400px;
width: 100%;
}
/deep/.el-card__body {
padding: 20px!important;
padding-top:0!important;
}
@media screen and (max-width: 768px) {
/deep/.el-card__body {
padding: 0!important;
}
}
.screen-full {
margin-right: 8px;
}
#switches p{
margin-top: 5px;
}
#switches p:first-child{
margin-top: 0;
}
#switches p span {
margin-left: 8px;
margin-right: 4px;
}
.vxe-cell p,.vxe-cell span{
margin: 0;
padding: 0;
}
/deep/.vxe-table .vxe-body--column{
line-height: 20px!important;
padding: 8px!important;
}
a.emphasis{
color:#495060
}
a.emphasis:hover{
color:#2d8cf0
}
</style>

View File

@ -0,0 +1,111 @@
import api from '@/common/api'
import { mapGetters, mapState } from 'vuex'
import { CONTEST_STATUS } from '@/common/constants'
export default {
methods: {
getContestRankData (page = 1, refresh = false) {
let offset = (page - 1) * this.limit
if (this.showChart && !refresh) {
this.$refs.chart.showLoading({maskColor: 'rgba(250, 250, 250, 0.8)'})
}
let params = {
offset,
limit: this.limit,
contest_id: this.$route.params.contestID,
force_refresh: this.forceUpdate ? '1' : '0'
}
api.getContestRank(params).then(res => {
if (this.showChart && !refresh) {
this.$refs.chart.hideLoading()
}
this.total = res.data.data.total
if (page === 1) {
this.applyToChart(res.data.data.results.slice(0, 10))
}
this.applyToTable(res.data.data.results)
})
},
handleAutoRefresh (status) {
if (status === true) {
this.refreshFunc = setInterval(() => {
this.page = 1
this.getContestRankData(1, true)
}, 10000)
} else {
clearInterval(this.refreshFunc)
}
}
},
computed: {
// ...mapGetters(['isContestAdmin']),
// ...mapState({
// 'contest': state => state.contest.contest,
// 'contestProblems': state => state.contest.contestProblems
// }),
// showChart: {
// get () {
// return this.$store.state.contest.itemVisible.chart
// },
// set (value) {
// this.$store.commit('changeContestItemVisible', {chart: value})
// }
// },
// showMenu: {
// get () {
// return this.$store.state.contest.itemVisible.menu
// },
// set (value) {
// this.$store.commit('changeContestItemVisible', {menu: value})
// this.$nextTick(() => {
// if (this.showChart) {
// this.$refs.chart.resize()
// }
// this.$refs.tableRank.handleResize()
// })
// }
// },
// showRealName: {
// get () {
// return this.$store.state.contest.itemVisible.realName
// },
// set (value) {
// this.$store.commit('changeContestItemVisible', {realName: value})
// if (value) {
// this.columns.splice(2, 0, {
// title: 'RealName',
// align: 'center',
// width: 150,
// render: (h, {row}) => {
// return h('span', row.user.real_name)
// }
// })
// } else {
// this.columns.splice(2, 1)
// }
// }
// },
forceUpdate: {
get () {
return this.$store.state.contest.forceUpdate
},
set (value) {
this.$store.commit('changeRankForceUpdate', {value: value})
}
},
// limit: {
// get () {
// return this.$store.state.contest.rankLimit
// },
// set (value) {
// this.$store.commit('changeContestRankLimit', {rankLimit: value})
// }
// },
// refreshDisabled () {
// return this.contest.status === CONTEST_STATUS.ENDED
// }
},
beforeDestroy () {
clearInterval(this.refreshFunc)
}
}

View File

@ -1,83 +0,0 @@
<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

@ -1,82 +0,0 @@
<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

@ -34,29 +34,38 @@
<vxe-table <vxe-table
border="inner" border="inner"
stripe stripe
auto-resize
@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="status" title="" width="30">
<template v-slot="{ row }" >
<el-tooltip :content="JUDGE_STATUS[row.myStatus].name" placement="top" >
<i class="el-icon-check" :style="getIconColor(row.myStatus)" v-if="row.myStatus==0" ></i>
<i class="el-icon-minus" :style="getIconColor(row.myStatus)" v-else-if="row.myStatus!=-10" ></i>
</el-tooltip>
</template>
</vxe-table-column>
<vxe-table-column field="pid" title="Problem ID" min-width="100"></vxe-table-column>
<vxe-table-column field="title" title="Title" width="300"> <vxe-table-column field="title" title="Title" min-width="250">
<template v-slot="{ row }"> <template v-slot="{ row }">
<a :href="getProblemUri(row.pid)" style="color:rgb(87, 163, 243);">{{row.title}}</a> <a :href="getProblemUri(row.pid)" style="color:rgb(87, 163, 243);font-size: 14px;">{{row.title}}</a>
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-table-column field="level" title="Level" width="150"> <vxe-table-column field="level" title="Level" min-width="100">
<template v-slot="{ row }"> <template v-slot="{ row }">
<span :class="getLevelColor(row.level)">{{PROBLEM_LEVEL[row.level].name}}</span> <span :class="getLevelColor(row.level)">{{PROBLEM_LEVEL[row.level].name}}</span>
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-table-column field="tag" title="Tag" width="250" type="html"> <vxe-table-column field="tag" title="Tag" min-width="200">
<template v-slot="{ row }"> <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> <span class="el-tag el-tag--medium el-tag--light is-hit" style="margin-right: 7px;margin-top:4px" v-for="tag in row.tags" :key="tag">{{tag}}</span>
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-table-column field="total" title="Total" width="100"></vxe-table-column> <vxe-table-column field="total" title="Total" min-width="80"></vxe-table-column>
<vxe-table-column field="ACRate" title="AC Rate" width="100"></vxe-table-column> <vxe-table-column field="ACRate" title="AC Rate" min-width="80"></vxe-table-column>
</vxe-table> </vxe-table>
</el-card> </el-card>
<Pagination :total="total" :page-size="limit" @on-change="pushRouter" :current.sync="query.page"></Pagination> <Pagination :total="total" :page-size="limit" @on-change="pushRouter" :current.sync="query.page"></Pagination>
@ -117,25 +126,25 @@
name:'模拟题' name:'模拟题'
}], }],
problemRecord:[ problemRecord:[
{status:'0',count:'70'}, {status:'0',count:70},
{status:'-1',count:'70'}, {status:'-1',count:70},
{status:'3',count:'70'}, {status:'3',count:70},
{status:'1',count:'70'}, {status:'1',count:70},
{status:'4',count:'70'}, {status:'4',count:70},
{status:'-3',count:'70'}, {status:'-3',count:70},
{status:'-2',count:'70'}, {status:'-2',count:70},
{status:'5',count:'70'}, {status:'5',count:70},
], ],
problemList: [ problemList: [
{pid:'1000',title:'测试标题',level:0, {myStatus:-10,pid:'1000',title:'测试标题',level:0,
tags:['简单题','模拟题'], tags:['简单题','模拟题'],
total:'10000',ACRate:'59.12%' total:'10000',ACRate:'59.12%'
}, },
{pid:'1000',title:'测试标题',level:1, {myStatus:-1,pid:'1000',title:'测试标题',level:1,
tags:['简单题','模拟题'], tags:['简单题','模拟题','递归题','递归题'],
total:'10000',ACRate:'59.12%' total:'10000',ACRate:'59.12%'
}, },
{pid:'1000',title:'测试标题',level:2, {myStatus:0,pid:'1000',title:'测试标题',level:2,
tags:['简单题','模拟题'], tags:['简单题','模拟题'],
total:'10000',ACRate:'59.12%' total:'10000',ACRate:'59.12%'
}, },
@ -239,6 +248,9 @@
}, },
getLevelColor(level){ getLevelColor(level){
return 'el-tag el-tag--small status-'+PROBLEM_LEVEL[level].color; return 'el-tag el-tag--small status-'+PROBLEM_LEVEL[level].color;
},
getIconColor(status){
return "font-weight: 600;font-size: 16px;color:"+JUDGE_STATUS[status].rgb;
} }
}, },
computed: { computed: {

View File

@ -1,24 +1,24 @@
<template> <template>
<el-row type="flex" justify="space-around"> <el-row type="flex" justify="space-around">
<el-col :span="22"> <el-col :span="24">
<el-card :padding="10"> <el-card :padding="10">
<div slot="header"><span class="panel-title">ACM Ranklist</span></div> <div slot="header"><span class="panel-title">ACM Ranklist</span></div>
<div class="echarts"> <div class="echarts">
<ECharts :options="options" ref="chart" auto-resize></ECharts> <ECharts :options="options" ref="chart" :autoresize="true"></ECharts>
</div> </div>
</el-card> </el-card>
<vxe-table :data="dataRank" :loading="loadingTable" align="center" highlight-hover-row style="font-weight: 500;"> <vxe-table :data="dataRank" :loading="loadingTable" align="center" highlight-hover-row auto-resize style="font-weight: 500;">
<vxe-table-column type="seq" width="100"></vxe-table-column> <vxe-table-column type="seq" min-width="50"></vxe-table-column>
<vxe-table-column field="username" title="User" width="197"> <vxe-table-column field="username" title="User" min-width="150">
<template v-slot="{ row }"> <template v-slot="{ row }">
<a :href="getInfoByUsername(row.username)" style="color:rgb(87, 163, 243);">{{row.username}}</a> <a :href="getInfoByUsername(row.username)" style="color:rgb(87, 163, 243);">{{row.username}}</a>
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-table-column field="nickname" title="Nickname" width="197"></vxe-table-column> <vxe-table-column field="nickname" title="Nickname" min-width="180"></vxe-table-column>
<vxe-table-column field="signature" title="Mood" width="197"></vxe-table-column> <vxe-table-column field="signature" title="Mood" min-width="180"></vxe-table-column>
<vxe-table-column field="ac" title="AC" width="197"></vxe-table-column> <vxe-table-column field="ac" title="AC" min-width="80"></vxe-table-column>
<vxe-table-column field="total" title="Total" width="197"></vxe-table-column> <vxe-table-column field="total" title="Total" min-width="80"></vxe-table-column>
<vxe-table-column field="rating" title="Rating" width="197"></vxe-table-column> <vxe-table-column field="rating" title="Rating" min-width="80"></vxe-table-column>
</vxe-table> </vxe-table>
<Pagination :total="total" :page-size.sync="limit" :current.sync="page" <Pagination :total="total" :page-size.sync="limit" :current.sync="page"
@ -45,12 +45,7 @@
limit: 30, limit: 30,
total: 0, total: 0,
loadingTable: false, loadingTable: false,
dataRank: [ 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: { options: {
tooltip: { tooltip: {
trigger: 'axis' trigger: 'axis'
@ -60,7 +55,9 @@
}, },
grid: { grid: {
x: '3%', x: '3%',
x2: '3%' x2: '3%',
left:'8%',
right:'8%'
}, },
toolbox: { toolbox: {
show: true, show: true,
@ -69,7 +66,7 @@
magicType: {show: true, type: ['line', 'bar', 'stack']}, magicType: {show: true, type: ['line', 'bar', 'stack']},
saveAsImage: {show: true} saveAsImage: {show: true}
}, },
right: '5%', right: '8%',
top:'5%' top:'5%'
}, },
calculable: true, calculable: true,
@ -83,14 +80,20 @@
showMaxLabel: true, showMaxLabel: true,
align: 'center', align: 'center',
formatter: (value, index) => { formatter: (value, index) => {
return utils.breakLongWords(value, 10) return utils.breakLongWords(value, 13)
} }
} }
} }
], ],
yAxis: [ yAxis: [
{ {
type: 'value' type: 'value',
axisLabel: {
rotate:50,
textStyle:{
fontSize:'12em',
},
}
} }
], ],
series: [ series: [
@ -120,6 +123,21 @@
}, },
mounted () { mounted () {
// this.getRankData(1) // this.getRankData(1)
let dataRank = [
{username:'20180308010541111',nickname:'Himit_ZHsssssssssssssssss我说你啵啵啵啵啵啵',signature:'Show me your code',ac:9999,total:9999,rating:'99.99%'},
{username:'测试名字为什么要那么长的呢',nickname:'图sddddddddd',signature:'Show me your code',ac:100,total:100,rating:'100%'},
{username:'20180308010541111',nickname:'Himit_ZH',signature:'Show me your code',ac:100,total:100,rating:'100%'},
{username:'测试名字为什么要那么长你有病啊',nickname:'Himit_ZH',signature:'Show me your code',ac:100,total:100,rating:'100%'},
{username:'测试名字为什么要那么长',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%'},
{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%'},
];
this.dataRank = dataRank;
this.changeCharts(dataRank);
}, },
methods: { methods: {
getRankData (page) { getRankData (page) {
@ -143,9 +161,9 @@
changeCharts (rankData) { changeCharts (rankData) {
let [usernames, acData, totalData] = [[], [], []] let [usernames, acData, totalData] = [[], [], []]
rankData.forEach(ele => { rankData.forEach(ele => {
usernames.push(ele.user.username) usernames.push(ele.username)
acData.push(ele.accepted_number) acData.push(ele.ac)
totalData.push(ele.submission_number) totalData.push(ele.total)
}) })
this.options.xAxis[0].data = usernames this.options.xAxis[0].data = usernames
this.options.series[0].data = acData this.options.series[0].data = acData
@ -154,6 +172,15 @@
getInfoByUsername(username){ getInfoByUsername(username){
return '/user-home/'+username; return '/user-home/'+username;
} }
},
computed:{
getFontSize(res){
let docEl = document.documentElement,
clientWidth = window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth;
if (!clientWidth) return;
let fontSize = 100 * (clientWidth / 1920);
return res*fontSize;
}
} }
} }
</script> </script>
@ -161,7 +188,12 @@
<style scoped> <style scoped>
.echarts { .echarts {
margin: 0 auto; margin: 0 auto;
width: 95%; width: 100%;
height: 400px; height: 400px;
} }
@media screen and (max-width: 768px) {
/deep/.el-card__body {
padding: 0!important;
}
}
</style> </style>

View File

@ -1,25 +1,25 @@
<template> <template>
<el-row type="flex" justify="space-around"> <el-row type="flex" justify="space-around">
<el-col :span="22"> <el-col :span="24">
<el-card :padding="10"> <el-card :padding="10">
<div slot="header"><span class="panel-title">OI Ranklist</span></div> <div slot="header"><span class="panel-title">OI Ranklist</span></div>
<div class="echarts"> <div class="echarts">
<ECharts :options="options" ref="chart" auto-resize></ECharts> <ECharts :options="options" ref="chart" auto-resize></ECharts>
</div> </div>
</el-card> </el-card>
<vxe-table :data="dataRank" :loading="loadingTable" align="center" highlight-hover-row style="font-weight: 500;"> <vxe-table :data="dataRank" :loading="loadingTable" align="center" highlight-hover-row auto-resize style="font-weight: 500;">
<vxe-table-column type="seq" width="100"></vxe-table-column> <vxe-table-column type="seq" min-width="50"></vxe-table-column>
<vxe-table-column field="username" title="User" width="168"> <vxe-table-column field="username" title="User" min-width="150">
<template v-slot="{ row }"> <template v-slot="{ row }">
<a :href="getInfoByUsername(row.username)" style="color:rgb(87, 163, 243);">{{row.username}}</a> <a :href="getInfoByUsername(row.username)" style="color:rgb(87, 163, 243);">{{row.username}}</a>
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-table-column field="nickname" title="Nickname" width="168"></vxe-table-column> <vxe-table-column field="nickname" title="Nickname" min-width="180"></vxe-table-column>
<vxe-table-column field="signature" title="Mood" width="173"></vxe-table-column> <vxe-table-column field="signature" title="Mood" min-width="180"></vxe-table-column>
<vxe-table-column field="score" title="Score" width="168"></vxe-table-column> <vxe-table-column field="score" title="Score" min-width="80"></vxe-table-column>
<vxe-table-column field="ac" title="AC" width="168"></vxe-table-column> <vxe-table-column field="ac" title="AC" min-width="80"></vxe-table-column>
<vxe-table-column field="total" title="Total" width="168"></vxe-table-column> <vxe-table-column field="total" title="Total" min-width="80"></vxe-table-column>
<vxe-table-column field="rating" title="Rating" width="168"></vxe-table-column> <vxe-table-column field="rating" title="Rating" min-width="80"></vxe-table-column>
</vxe-table> </vxe-table>
<Pagination :total="total" :page-size.sync="limit" :current.sync="page" <Pagination :total="total" :page-size.sync="limit" :current.sync="page"
@on-change="getRankData" @on-change="getRankData"
@ -44,11 +44,7 @@
page: 1, page: 1,
limit: 30, limit: 30,
total: 0, total: 0,
dataRank: [ 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: { options: {
tooltip: { tooltip: {
trigger: 'axis' trigger: 'axis'
@ -58,7 +54,9 @@
}, },
grid: { grid: {
x: '3%', x: '3%',
x2: '3%' x2: '3%',
left:'8%',
right:'8%'
}, },
toolbox: { toolbox: {
show: true, show: true,
@ -67,8 +65,8 @@
magicType: {show: true, type: ['line', 'bar']}, magicType: {show: true, type: ['line', 'bar']},
saveAsImage: {show: true} saveAsImage: {show: true}
}, },
right: '5%', right: '8%',
top:'5%' top:'5%',
}, },
calculable: true, calculable: true,
xAxis: [ xAxis: [
@ -92,7 +90,13 @@
], ],
yAxis: [ yAxis: [
{ {
type: 'value' type: 'value',
axisLabel: {
rotate:50,
textStyle:{
fontSize:'12em',
},
}
} }
], ],
series: [ series: [
@ -113,6 +117,20 @@
}, },
mounted () { mounted () {
// this.getRankData(1) // this.getRankData(1)
let data = [
{username:'root111',nickname:'Himit_ZH',signature:'Show me your code',score:100000,ac:100,total:100,rating:'100%'},
{username:'测试一下不会真的有人取那么长的名字吧',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%'},
{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%'},
{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%'},
];
this.dataRank = data;
this.changeCharts(data);
}, },
methods: { methods: {
getRankData (page) { getRankData (page) {
@ -131,8 +149,8 @@
changeCharts (rankData) { changeCharts (rankData) {
let [usernames, scores] = [[], []] let [usernames, scores] = [[], []]
rankData.forEach(ele => { rankData.forEach(ele => {
usernames.push(ele.user.username) usernames.push(ele.username)
scores.push(ele.total_score) scores.push(ele.score)
}) })
this.options.xAxis[0].data = usernames this.options.xAxis[0].data = usernames
this.options.series[0].data = scores this.options.series[0].data = scores
@ -147,7 +165,12 @@
<style scoped> <style scoped>
.echarts { .echarts {
margin: 0 auto; margin: 0 auto;
width: 95%; width: 100%;
height: 400px; height: 400px;
} }
@media screen and (max-width: 768px) {
/deep/.el-card__body {
padding: 0!important;
}
}
</style> </style>

View File

@ -21,14 +21,14 @@
</el-col> </el-col>
<el-col v-if="submission.data && !isCE" :span="22"> <el-col v-if="submission.data && !isCE" :span="22">
<vxe-table align="center" :data="submission.data" stripe style="padding-top: 13px;"> <vxe-table align="center" :data="submission.data" stripe auto-resize style="padding-top: 13px;">
<vxe-table-column field="sid" title="ID" width="213"></vxe-table-column> <vxe-table-column field="sid" title="ID" min-width="100"></vxe-table-column>
<vxe-table-column <vxe-table-column
field="stime" field="stime"
title="Submit time" title="Submit time"
width="213" min-width="150"
></vxe-table-column> ></vxe-table-column>
<vxe-table-column field="pid" title="Problem ID" width="213"> <vxe-table-column field="pid" title="Problem ID" min-width="100">
<template v-slot="{ row }"> <template v-slot="{ row }">
<a <a
:href="getProblemUri(row.pid)" :href="getProblemUri(row.pid)"
@ -37,7 +37,7 @@
> >
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-table-column field="status" title="Status" width="213"> <vxe-table-column field="status" title="Status" min-width="100">
<template v-slot="{ row }"> <template v-slot="{ row }">
<span :class="getStatusColor(row.status)">{{ <span :class="getStatusColor(row.status)">{{
JUDGE_STATUS[row.status].name JUDGE_STATUS[row.status].name
@ -47,12 +47,12 @@
<vxe-table-column <vxe-table-column
field="time" field="time"
title="Time" title="Time"
width="213" min-width="64"
></vxe-table-column> ></vxe-table-column>
<vxe-table-column <vxe-table-column
field="memory" field="memory"
title="Memory" title="Memory"
width="213" min-width="96"
></vxe-table-column> ></vxe-table-column>
</vxe-table> </vxe-table>
</el-col> </el-col>
@ -120,8 +120,8 @@ export default {
stime: "2020-08-08 16:00:00", stime: "2020-08-08 16:00:00",
pid: "1001", pid: "1001",
status: 0, status: 0,
time: "4ms", time: "1000ms",
memory: "3MB", memory: "9999MB",
language:'Java', language:'Java',
author: "Himit_ZH", author: "Himit_ZH",
err_info:'CE错误', err_info:'CE错误',

View File

@ -2,348 +2,421 @@
<div class="flex-container"> <div class="flex-container">
<div id="main"> <div id="main">
<el-card shadow> <el-card shadow>
<div slot="header"> <div slot="header">
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :md="2" :lg="2"> <el-col :md="2" :lg="2">
<span class="panel-title hidden-xs-only" >Status</span> <span class="panel-title hidden-xs-only">Status</span>
</el-col> </el-col>
<el-col :xs="16" :md="10" :lg="10"> <el-col :xs="16" :md="10" :lg="10">
<el-switch <el-switch
style="display: block" style="display: block"
v-model="formFilter.myself" v-model="formFilter.myself"
active-color="#ff4949" active-color="#ff4949"
active-text="Mine" active-text="Mine"
:width="40" :width="40"
inactive-text="All"> inactive-text="All"
</el-switch> >
</el-col> </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"
@command="handleStatusChange" @command="handleStatusChange"
placement="bottom" placement="bottom"
trigger="hover" trigger="hover"
> >
<span class="el-dropdown-link"> <span class="el-dropdown-link">
{{formFilter.status === '' ? 'Status' : formFilter.status}} {{ formFilter.status === "" ? "Status" : formFilter.status }}
<i class="el-icon-caret-bottom"></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>
<el-dropdown-item v-for="result in Object.keys(JUDGE_STATUS)" :key="result" :command="result"> <el-dropdown-item
{{JUDGE_STATUS[result].name}} v-for="result in Object.keys(JUDGE_STATUS)"
</el-dropdown-item> :key="result"
</el-dropdown-menu> :command="result"
</el-dropdown> >
</el-col> {{ JUDGE_STATUS[result].name }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
<el-col :xs="24" :md="4" :lg="4" class="search"> <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> <vxe-input
</el-col> v-model="formFilter.pid"
<el-col :xs="24" :md="4" :lg="4" class="search"> placeholder="Enter Problem ID"
<vxe-input v-model="formFilter.pid" placeholder="Enter Author" type="search" size="medium" @search-click="filterByKeyword"></vxe-input> type="search"
</el-col> size="medium"
</el-row> @search-click="filterByKeyword"
</div> ></vxe-input>
<vxe-table </el-col>
border="inner" <el-col :xs="24" :md="4" :lg="4" class="search">
stripe <vxe-input
highlight-current-row v-model="formFilter.pid"
highlight-hover-row placeholder="Enter Author"
align="center" type="search"
:row-class-name="tableRowClassName" size="medium"
:data="submissions"> @search-click="filterByKeyword"
<vxe-table-column field="sid" title="ID" width="150"></vxe-table-column> ></vxe-input>
<vxe-table-column field="stime" title="Submit time" width="150"></vxe-table-column> </el-col>
<vxe-table-column field="pid" title="Problem ID" width="150"> </el-row>
<template v-slot="{ row }"> </div>
<a :href="getProblemUri(row.pid)" style="color:rgb(87, 163, 243);">{{row.pid}}</a> <vxe-table
</template> border="inner"
</vxe-table-column> stripe
<vxe-table-column field="status" title="Status" width="150"> highlight-current-row
<template v-slot="{ row }"> highlight-hover-row
<span :class="getStatusColor(row.status)">{{JUDGE_STATUS[row.status].name}}</span> align="center"
</template> auto-resize
</vxe-table-column> :row-class-name="tableRowClassName"
<vxe-table-column field="time" title="Time" width="150"></vxe-table-column> :data="submissions"
<vxe-table-column field="memory" title="Memory" width="150"></vxe-table-column> >
<vxe-table-column field="language" title="Language" width="150"> <vxe-table-column
<template v-slot="{ row }"> field="sid"
<el-tooltip class="item" effect="dark" content="查看提交详情" placement="top"> title="Run ID"
<el-button type="text" @click="showSubmitDetail(row)">{{ row.language }}</el-button> min-width="100"
</el-tooltip> ></vxe-table-column>
</template> <vxe-table-column
</vxe-table-column> field="stime"
<vxe-table-column field="judger" title="Judger" width="150"></vxe-table-column> title="Submit time"
<vxe-table-column field="author" title="Author" width="150"></vxe-table-column> min-width="150"
</vxe-table> ></vxe-table-column>
</el-card> <vxe-table-column field="pid" title="Problem ID" min-width="100">
<Pagination :total="total" :page-size="limit" @on-change="changeRoute" :current.sync="page"></Pagination> <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" min-width="100">
<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"
min-width="64"
></vxe-table-column>
<vxe-table-column
field="memory"
title="Memory"
min-width="96"
></vxe-table-column>
<vxe-table-column field="language" title="Language" min-width="100">
<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"
min-width="100"
></vxe-table-column>
<vxe-table-column
field="author"
title="Author"
min-width="100"
></vxe-table-column>
</vxe-table>
</el-card>
<Pagination
:total="total"
:page-size="limit"
@on-change="changeRoute"
:current.sync="page"
></Pagination>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from "vuex";
import api from '@/common/api' import api from "@/common/api";
import { JUDGE_STATUS, USER_TYPE } from '@/common/constants' import { JUDGE_STATUS, USER_TYPE } from "@/common/constants";
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 {
formFilter: { formFilter: {
myself: false, myself: false,
status: '', status: "",
username: '', username: "",
pid:'' pid: "",
},
loadingTable: false,
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",
}, },
loadingTable: false, {
submissions: [ sid: 1001,
{ stime: "2020-08-08 16:00:00",
sid:1000, pid: "1001",
stime:'2020-08-08 16:00:00', status: 0,
pid:'1001', time: "1000ms",
status:0, memory: "3MB",
time:'4ms', language: "C++",
memory:'3MB', judger: "192.168.1.1",
language:'C++', author: "Himit_Z",
judger:'192.168.1.1', },
author:'Himit_ZH' {
}, sid: 1000,
{ stime: "2020-08-08 16:00:00",
sid:1001, pid: "1001",
stime:'2020-08-08 16:00:00', status: 0,
pid:'1001', time: "4ms",
status:0, memory: "3MB",
time:'4ms', language: "C++",
memory:'3MB', judger: "192.168.1.1",
language:'C++', author: "Himit_H",
judger:'192.168.1.1', },
author:'Himit_Z' ],
}, total: 30,
{ limit: 15,
sid:1000, page: 1,
stime:'2020-08-08 16:00:00', contestID: "",
pid:'1001', problemID: "",
status:0, routeName: "",
time:'4ms', JUDGE_STATUS: "",
memory:'3MB', rejudge_column: false,
language:'C++', };
judger:'192.168.1.1', },
author:'Himit_H' mounted() {
} this.init();
this.JUDGE_STATUS = Object.assign({}, JUDGE_STATUS);
], // submitting
total: 30, delete this.JUDGE_STATUS["9"];
limit: 15, delete this.JUDGE_STATUS["2"];
page: 1, },
contestID: '', methods: {
problemID: '', init() {
routeName: '', this.contestID = this.$route.params.contestID;
JUDGE_STATUS: '', let query = this.$route.query;
rejudge_column: false this.problemID = query.problemID;
this.formFilter.myself = query.myself === "1";
this.formFilter.status = query.status || "";
this.formFilter.username = query.username || "";
this.page = parseInt(query.page) || 1;
if (this.page < 1) {
this.page = 1;
} }
this.routeName = this.$route.name;
this.getSubmissions();
}, },
mounted () { buildQuery() {
this.init() return {
this.JUDGE_STATUS = Object.assign({}, JUDGE_STATUS) myself: this.formFilter.myself === true ? "1" : "0",
// submitting status: this.formFilter.status,
delete this.JUDGE_STATUS['9'] username: this.formFilter.username,
delete this.JUDGE_STATUS['2'] page: this.page,
};
}, },
methods: { getSubmissions() {
init () { let params = this.buildQuery();
this.contestID = this.$route.params.contestID params.contest_id = this.contestID;
let query = this.$route.query params.problem_id = this.problemID;
this.problemID = query.problemID let offset = (this.page - 1) * this.limit;
this.formFilter.myself = query.myself === '1' let func = this.contestID
this.formFilter.status = query.status || '' ? "getContestSubmissionList"
this.formFilter.username = query.username || '' : "getSubmissionList";
this.page = parseInt(query.page) || 1 this.loadingTable = true;
if (this.page < 1) { api[func](offset, this.limit, params)
this.page = 1 .then((res) => {
} let data = res.data.data;
this.routeName = this.$route.name
this.getSubmissions()
},
buildQuery () {
return {
myself: this.formFilter.myself === true ? '1' : '0',
status: this.formFilter.status,
username: this.formFilter.username,
page: this.page
}
},
getSubmissions () {
let params = this.buildQuery()
params.contest_id = this.contestID
params.problem_id = this.problemID
let offset = (this.page - 1) * this.limit
let func = this.contestID ? 'getContestSubmissionList' : 'getSubmissionList'
this.loadingTable = true
api[func](offset, this.limit, params).then(res => {
let data = res.data.data
for (let v of data.results) { for (let v of data.results) {
v.loading = false v.loading = false;
} }
this.adjustRejudgeColumn() this.adjustRejudgeColumn();
this.loadingTable = false this.loadingTable = false;
this.submissions = data.results this.submissions = data.results;
this.total = data.total this.total = data.total;
}).catch(() => {
this.loadingTable = false
}) })
}, .catch(() => {
// route routeroute history this.loadingTable = false;
changeRoute () { });
let query = utils.filterEmptyValue(this.buildQuery()) },
query.contestID = this.contestID // route routeroute history
query.problemID = this.problemID changeRoute() {
let routeName = query.contestID ? 'contest-submission-list' : 'submission-list' let query = utils.filterEmptyValue(this.buildQuery());
this.$router.push({ query.contestID = this.contestID;
name: routeName, query.problemID = this.problemID;
query: utils.filterEmptyValue(query) let routeName = query.contestID
}) ? "contest-submission-list"
}, : "submission-list";
goRoute (route) { this.$router.push({
this.$router.push(route) name: routeName,
}, query: utils.filterEmptyValue(query),
adjustRejudgeColumn () { });
if (!this.rejudgeColumnVisible || this.rejudge_column) { },
return goRoute(route) {
} this.$router.push(route);
const judgeColumn = { },
title: this.$i18n.t('m.Option'), adjustRejudgeColumn() {
fixed: 'right', if (!this.rejudgeColumnVisible || this.rejudge_column) {
align: 'center', return;
width: 90, }
render: (h, params) => { const judgeColumn = {
return h('Button', { title: this.$i18n.t("m.Option"),
fixed: "right",
align: "center",
width: 90,
render: (h, params) => {
return h(
"Button",
{
props: { props: {
type: 'primary', type: "primary",
size: 'small', size: "small",
loading: params.row.loading loading: params.row.loading,
}, },
on: { on: {
click: () => { click: () => {
this.handleRejudge(params.row.id, params.index) this.handleRejudge(params.row.id, params.index);
} },
} },
}, this.$i18n.t('m.Rejudge')) },
} this.$i18n.t("m.Rejudge")
} );
this.columns.push(judgeColumn) },
this.rejudge_column = true };
}, this.columns.push(judgeColumn);
handleStatusChange (status) { this.rejudge_column = true;
this.page = 1 },
this.formFilter.status = status handleStatusChange(status) {
this.changeRoute() this.page = 1;
}, this.formFilter.status = status;
handleQueryChange () { this.changeRoute();
this.page = 1 },
this.changeRoute() handleQueryChange() {
}, this.page = 1;
handleRejudge (id, index) { this.changeRoute();
this.submissions[index].loading = true },
api.submissionRejudge(id).then(res => { handleRejudge(id, index) {
this.submissions[index].loading = false this.submissions[index].loading = true;
this.$success('Succeeded') api.submissionRejudge(id).then(
this.getSubmissions() (res) => {
}, () => { this.submissions[index].loading = false;
this.submissions[index].loading = false this.$success("Succeeded");
}) this.getSubmissions();
}, },
showSubmitDetail (row) { () => {
this.selectSubmitRow = row; this.submissions[index].loading = false;
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';
} }
);
},
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: { },
...mapGetters(['isAuthenticated', 'userInfo']), computed: {
title () { ...mapGetters(["isAuthenticated", "userInfo"]),
if (!this.contestID) { title() {
return 'Status' if (!this.contestID) {
} else if (this.problemID) { return "Status";
return 'Problem Submissions' } else if (this.problemID) {
} else { return "Problem Submissions";
return 'Submissions' } else {
} return "Submissions";
},
status () {
return this.formFilter.status === '' ? 'Status' : JUDGE_STATUS[this.formFilter.status].name.replace(/ /g, '_')
},
rejudgeColumnVisible () {
return !this.contestID && this.userInfo.role === USER_TYPE.SUPER_ADMIN
} }
}, },
watch: { status() {
'$route' (newVal, oldVal) { return this.formFilter.status === ""
if (newVal !== oldVal) { ? "Status"
this.init() : JUDGE_STATUS[this.formFilter.status].name.replace(/ /g, "_");
} },
}, rejudgeColumnVisible() {
'rejudgeColumnVisible' () { return !this.contestID && this.userInfo.role === USER_TYPE.SUPER_ADMIN;
this.adjustRejudgeColumn() },
}, },
'isAuthenticated' () { watch: {
this.init() $route(newVal, oldVal) {
if (newVal !== oldVal) {
this.init();
} }
} },
} rejudgeColumnVisible() {
this.adjustRejudgeColumn();
},
isAuthenticated() {
this.init();
},
},
};
</script> </script>
<style scoped > <style scoped >
@media only screen and (max-width: 767px) {
.search {
margin-top: 20px;
}
}
@media only screen and (max-width: 767px){ .flex-container #main {
.search{ flex: auto;
margin-top: 20px; }
} .flex-container .filter {
} margin-right: -10px;
}
.flex-container #main { .flex-container #contest-menu {
flex: auto; flex: none;
margin-right: 18px; width: 210px;
} }
.flex-container .filter { /deep/ .el-card__header {
margin-right: -10px; border-bottom: 0px;
} padding-bottom: 0px;
.flex-container #contest-menu { text-align: center;
flex: none; }
width: 210px; /deep/ .el-button {
} padding: 0;
/deep/ .el-card__header { }
border-bottom: 0px; /deep/ .el-dialog {
padding-bottom: 0px; border-radius: 6px !important;
text-align: center; text-align: center;
} }
/deep/ .el-button{ /deep/ .el-switch {
padding: 0; padding-top: 6px;
} }
/deep/ .el-dialog {
border-radius: 6px !important;
text-align: center;
}
/deep/ .el-switch{
padding-top: 6px;
}
</style> </style>