This commit is contained in:
LiYang 2017-04-25 15:39:41 +08:00
parent 8d6c212c33
commit f061a648bf
43 changed files with 2956 additions and 322 deletions

View File

@ -1,14 +1,5 @@
{
"presets": [
["env", { "modules": false }],
"stage-2"
],
"presets": ["es2015", "stage-2"],
"plugins": ["transform-runtime"],
"comments": false,
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": [ "istanbul" ]
}
}
"comments": false
}

View File

@ -1,14 +1,9 @@
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: 'standard',
// required to lint *.vue files

View File

@ -1,8 +0,0 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
// to edit target browsers: use "browserlist" field in package.json
"autoprefixer": {}
}
}

View File

@ -1,35 +1,37 @@
// https://github.com/shelljs/shelljs
require('./check-versions')()
require('shelljs/global')
env.NODE_ENV = 'production'
process.env.NODE_ENV = 'production'
var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var ora = require('ora')
var webpack = require('webpack')
var webpackConfig = require('./webpack.prod.conf')
console.log(
' Tip:\n' +
' Built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
)
var spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
rm('-rf', assetsPath)
mkdir('-p', assetsPath)
cp('-R', 'static/*', assetsPath)
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
webpack(webpackConfig, function (err, stats) {
spinner.stop()
require('fs').writeFileSync('stat.json', JSON.stringify(stats.toJson()))
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n')
})

View File

@ -1,9 +1,9 @@
var chalk = require('chalk')
var semver = require('semver')
var chalk = require('chalk')
var packageConfig = require('../package.json')
var shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
var exec = function (cmd) {
return require('child_process')
.execSync(cmd).toString().trim()
}
var versionRequirements = [
@ -12,15 +12,12 @@ var versionRequirements = [
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
},
]
if (shell.which('npm')) {
versionRequirements.push({
{
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
}
]
module.exports = function () {
var warnings = []

View File

@ -1,21 +1,17 @@
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
if (!process.env.NODE_ENV) process.env.NODE_ENV = config.dev.env
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var opn = require('opn')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')
var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
@ -25,12 +21,13 @@ var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
stats: {
colors: true,
chunks: false
}
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
var hotMiddleware = require('webpack-hot-middleware')(compiler)
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
@ -45,7 +42,7 @@ Object.keys(proxyTable).forEach(function (context) {
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
app.use(proxyMiddleware(context, options))
})
// handle fallback for HTML5 history API
@ -62,28 +59,12 @@ app.use(hotMiddleware)
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})
console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
console.log('> Listening at ' + uri + '\n')
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
module.exports = app.listen(port, function (err) {
if (err) {
console.log(err)
return
}
_resolve()
var uri = 'http://localhost:' + port
console.log('Listening at ' + uri + '\n')
opn(uri)
})
var server = app.listen(port)
module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}

View File

@ -11,48 +11,38 @@ exports.assetsPath = function (_path) {
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
function generateLoaders (loaders) {
var sourceLoader = loaders.map(function (loader) {
var extraParamChar
if (/\?/.test(loader)) {
loader = loader.replace(/\?/, '-loader?')
extraParamChar = '&'
} else {
loader = loader + '-loader'
extraParamChar = '?'
}
return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '')
}).join('!')
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
return ExtractTextPlugin.extract('vue-style-loader', sourceLoader)
} else {
return ['vue-style-loader'].concat(loaders)
return ['vue-style-loader', sourceLoader].join('!')
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
css: generateLoaders(['css']),
postcss: generateLoaders(['css']),
less: generateLoaders(['css', 'less']),
sass: generateLoaders(['css', 'sass?indentedSyntax']),
scss: generateLoaders(['css', 'sass']),
stylus: generateLoaders(['css', 'stylus']),
styl: generateLoaders(['css', 'stylus'])
}
}
@ -64,7 +54,7 @@ exports.styleLoaders = function (options) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
loader: loader
})
}
return output

View File

@ -1,12 +0,0 @@
var utils = require('./utils')
var config = require('../config')
var isProduction = process.env.NODE_ENV === 'production'
module.exports = {
loaders: utils.cssLoaders({
sourceMap: isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap,
extract: isProduction
})
}

View File

@ -1,11 +1,15 @@
var path = require('path')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
// various preprocessor loaders added to vue-loader at the end of this file
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
var webpack = require('webpack')
module.exports = {
entry: {
@ -13,55 +17,84 @@ module.exports = {
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
extensions: ['', '.js', '.vue'],
fallback: [path.join(__dirname, '../node_modules')],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
'vue$': 'vue/dist/vue',
'src': path.resolve(__dirname, '../src'),
'assets': path.resolve(__dirname, '../src/assets'),
'components': path.resolve(__dirname, '../src/components')
}
},
resolveLoader: {
fallback: [path.join(__dirname, '../node_modules')]
},
module: {
rules: [
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
preLoaders: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
loader: 'eslint',
include: projectRoot,
exclude: /node_modules/
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
loader: 'eslint',
include: projectRoot,
exclude: /node_modules/
}
],
loaders: [
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.js$/,
loader: 'babel',
include: projectRoot,
exclude: /node_modules/
},
{
test: /\.json$/,
loader: 'json'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
loader: 'url',
query: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
loader: 'url',
query: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
},
eslint: {
formatter: require('eslint-friendly-formatter')
},
vue: {
loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }),
postcss: [
require('autoprefixer')({
browsers: ['last 2 versions']
})
]
},
plugins: [
// https://github.com/webpack/webpack/issues/87
new webpack.ContextReplacementPlugin(/moment\/locale$/, /en-gb/),
new webpack.ContextReplacementPlugin(/codemirror\/mode$/, /clike\/clike|meta|python\/python|javascript\/javascript/)
]
}

View File

@ -1,10 +1,9 @@
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var webpack = require('webpack')
var merge = require('webpack-merge')
var utils = require('./utils')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
@ -13,23 +12,23 @@ Object.keys(baseWebpackConfig.entry).forEach(function (name) {
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
// eval-source-map is faster for development
devtool: '#eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.NoErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
new FriendlyErrorsPlugin()
})
]
})

View File

@ -1,22 +1,18 @@
var path = require('path')
var config = require('../config')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = config.build.env
var HtmlWebpackPlugin = require('html-webpack-plugin')
var env = process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
@ -24,33 +20,32 @@ var webpackConfig = merge(baseWebpackConfig, {
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
vue: {
loaders: utils.cssLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
// http://vuejs.github.io/vue-loader/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
new webpack.optimize.OccurenceOrderPlugin(),
// extract css into its own file
new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
: config.build.index,
template: 'index.html',
inject: true,
minify: {
@ -82,15 +77,7 @@ var webpackConfig = merge(baseWebpackConfig, {
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
})
]
})
@ -112,9 +99,4 @@ if (config.build.productionGzip) {
)
}
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

View File

@ -14,20 +14,19 @@ module.exports = {
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
productionGzipExtensions: ['js', 'css']
},
dev: {
env: require('./dev.env'),
port: 8080,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
proxyTable: {
"/api": {
target: process.env.TARGET,
changeOrigin: true
}
},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)

View File

@ -1,3 +1,10 @@
let date = require('moment')().format('YYYYMMDD')
let commit = require('child_process').execSync('git rev-parse HEAD').toString().slice(0, 5)
let version = `"${date}-${commit}"`
console.log(`current version is ${version}`)
module.exports = {
NODE_ENV: '"production"'
NODE_ENV: '"production"',
VERSION: version
}

6
oj/config/test.env.js Normal file
View File

@ -0,0 +1,6 @@
var merge = require('webpack-merge')
var devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})

View File

@ -1,8 +1,10 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>oj</title>
<title>OnlineJudge Admin</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
</head>
<body>
<div id="app"></div>

View File

@ -1,23 +1,80 @@
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view></router-view>
<div>
<div>
<SideMenu></SideMenu>
</div>
<div class="content-app">
<router-view></router-view>
<div class="footer">
Build Version: {{ version }}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'app'
}
import 'font-awesome/css/font-awesome.min.css'
import SideMenu from './components/SideMenu.vue'
import api from './api.js'
export default {
name: 'app',
data () {
return {
version: process.env.VERSION
}
},
components: {
SideMenu
},
methods: {},
mounted () {
api.login('root', 'rootroot').then(res => {})
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
<style lang="less">
body {
margin: 0;
background-color: rgb(236, 242, 247)!important;
}
a {
background-color: transparent;
-webkit-text-decoration-skip: objects
}
a:active, a:hover {
outline-width: 0
}
img {
border-style: none
}
body {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
overflow: auto;
font-weight: 400;
-webkit-font-smoothing: antialiased;
background-color: #EDECEC;
overflow-y: scroll;
}
* {
box-sizing: border-box;
}
.content-app {
padding-top: 20px;
padding-right:10px;
padding-left: 210px;
}
.footer {
margin: 15px;
text-align: center;
font-size: small;
}
</style>

308
oj/src/api.js Normal file
View File

@ -0,0 +1,308 @@
import Vue from 'vue'
import VueResource from 'vue-resource'
Vue.use(VueResource)
Vue.http.options.root = '/api'
Vue.http.options.emulateJSON = false
function getCookie (name) {
let allCookies = document.cookie.split('; ')
for (let i = 0; i < allCookies.length; i++) {
let cookie = allCookies[i].split('=')
if (cookie[0] === name) {
return cookie[1]
} else {
return ''
}
}
}
Vue.http.interceptors.push((request, next) => {
request.headers.set('X-CSRFToken', getCookie('csrftoken'))
next()
})
export default {
// 登录
login (username, password) {
return ajax('login', 'get', {
options: {
params: {
username,
password
}
}
})
},
// 获取公告列表
getAnnouncementList (offset, limit) {
return ajax('admin/announcement', 'get', {
options: {
params: {
paging: true,
offset,
limit
}
}
})
},
// 删除公告
deleteAnnouncement (id) {
return ajax('admin/announcement', 'delete', {
options: {
params: {
id
}
}
})
},
// 修改公告
modifyAnnouncement (id, title, content, visible) {
return ajax('admin/announcement', 'put', {
body: {
id,
title,
content,
visible
}
})
},
// 添加公告
createAnnouncement (title, content, visible) {
return ajax('admin/announcement', 'post', {
body: {
title,
content,
visible
}
})
},
// 获取用户列表
getUserList (offset, limit, keyword) {
let params = {paging: true, offset, limit}
if (keyword) {
params.keyword = keyword
}
return ajax('admin/user', 'get', {
options: {
params: params
}
})
},
// 获取单个用户信息
getUser (id) {
return ajax('admin/user', 'get', {
options: {
params: {
id
}
}
})
},
// 编辑用户
editUser (body) {
return ajax('admin/user', 'put', {
body
})
},
getLanguages () {
return ajax('languages', 'get')
},
getSMTPConfig () {
return ajax('admin/smtp', 'get')
},
createSMTPConfig (body) {
return ajax('admin/smtp', 'post', {
body
})
},
editSMTPConfig (body) {
return ajax('admin/smtp', 'put', {
body
})
},
getWebsiteConfig () {
return ajax('admin/website', 'get')
},
editWebsiteConfig (config) {
return ajax('admin/website', 'post', {
body: config
})
},
getJudgeServer () {
return ajax('admin/judge_server', 'get')
},
deleteJudgeServer (hostname) {
return ajax('admin/judge_server', 'delete', {
options: {
params: {
hostname: hostname
}
}
})
},
createContest (body) {
return ajax('admin/contest', 'post', {
body: body
})
},
getContest (id) {
return ajax('admin/contest', 'get', {
options: {
params: {
id
}
}
})
},
editContest (body) {
return ajax('admin/contest', 'put', {
body
})
},
getContestList (offset, limit, keyword) {
let params = {paging: true, offset, limit}
if (keyword) {
params.keyword = keyword
}
return ajax('admin/contest', 'get', {
options: {
params: params
}
})
},
getContestAnnouncementList (contestId) {
return ajax('admin/contest/announcement', 'get', {
options: {
params: {
contest_id: contestId
}
}
})
},
createContestAnnouncement (body) {
return ajax('admin/contest/announcement', 'post', {
body
})
},
deleteContestAnnouncement (id) {
return ajax('admin/contest/announcement', 'delete', {
options: {
params: {
id
}
}
})
},
getProblemTagList () {
return ajax('problem/tags', 'get')
},
createProblem (body) {
return ajax('admin/problem', 'post', {
body
})
},
editProblem (body) {
return ajax('admin/problem', 'put', {
body
})
},
getProblem (id) {
return ajax('admin/problem', 'get', {
options: {
params: {
id
}
}
})
},
getProblemList (offset, limit, keyword) {
let params = {paging: true, offset, limit}
if (keyword) {
params.keyword = keyword
}
return ajax('admin/problem', 'get', {
options: {
params: params
}
})
},
getContestProblemList (offset, limit, keyword, contestId) {
let params = {paging: true, offset, limit, contest_id: contestId}
if (keyword) {
params.keyword = keyword
}
return ajax('admin/contest/problem', 'get', {
options: {
params: params
}
})
},
getContestProblem (id) {
return ajax('admin/contest/problem', 'get', {
options: {
params: {
id
}
}
})
},
createContestProblem (body) {
return ajax('admin/contest/problem', 'post', {
body
})
},
editContestProblem (body) {
return ajax('admin/contest/problem', 'put', {
body
})
}
}
/**
ajax 请求
@param url
@param type get|post|put|jsonp ....
@param options options = {
body: request body
options: ..,
succCallBack: Function
errCallBack: Function
}
@return Promise
*/
function ajax (url, type, options) {
return new Promise(function (resolve, reject) {
options = options || {}
if (options.body === undefined) {
options.body = options.options
options.options = undefined
}
Vue.http[type](url, options.body, options.options).then(res => {
// 出错了
if (res.data.error !== null) {
Vue.prototype.$error(res.data.data)
reject(res)
if (options.errCallBack !== undefined) {
options.errCallBack(res)
}
} else {
// 请求成功
resolve(res)
if (options.succCallBack !== undefined) {
options.succCallBack(res)
} else if (type !== 'get') {
Vue.prototype.$success()
}
}
}, res => {
// 请求失败
reject(res)
if (options.errCallBack !== undefined) {
options.errCallBack(res)
} else {
Vue.prototype.$error('Network Error')
}
})
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

1
oj/src/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

@ -0,0 +1,66 @@
<template>
<codemirror v-model="currentValue" :options="options" ref="editor"></codemirror>
</template>
<script>
import {codemirror} from 'vue-codemirror'
import 'codemirror/mode/clike/clike.js'
import 'codemirror/mode/python/python.js'
export default{
name: 'CodeMirror',
data () {
return {
currentValue: '',
options: {
mode: 'text/x-csrc',
lineNumbers: true,
line: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autofocus: true
}
}
},
components: {
codemirror
},
props: {
value: {
type: String,
default: ''
},
mode: {
type: String,
default: 'text/x-csrc'
}
},
mounted () {
this.currentValue = this.value
this.$refs.editor.editor.setOption('mode', this.mode)
},
watch: {
'value' (val) {
if (this.currentValue !== val) {
this.currentValue = val
}
},
'currentValue' (newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit('change', newVal)
this.$emit('input', newVal)
}
},
'mode' (newVal) {
this.$refs.editor.editor.setOption('mode', newVal)
}
}
}
</script>
<style>
.CodeMirror {
height: auto !important;
}
.CodeMirror-scroll {
min-height:300px;
}
</style>

View File

@ -1,53 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>Essential Links</h2>
<ul>
<li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
<li><a href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
<br>
<li><a href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This Template</a></li>
</ul>
<h2>Ecosystem</h2>
<ul>
<li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
<li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
<li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'hello',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="panel" :class="{'small': small}">
<header>
<h2>{{title}}</h2>
<div class="header_right">
<slot name="header"></slot>
</div>
</header>
<div class="body">
<slot></slot>
</div>
</div>
</template>
<script>
export default{
name: 'Panel',
props: {
title: {
type: String,
required: true
},
small: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped lang="less">
.panel{
margin-bottom: 20px;
background-color: #fff;
border: 1px solid transparent;
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0,0,0,.05);
&.small{
max-width: 830px;
min-width: 700px;
margin-left: 20px;
margin-top: 10px;
}
header{
position: relative;
z-index: 10;
>h2{
margin: 0;
color: #333;
border-color: #ddd;
font-size: 18px;
font-weight: 300;
letter-spacing: 0.025em;
height: 66px;
line-height: 45px;
padding: 10px 15px;
border-bottom: 1px solid #eee;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
>.header_right{
position: absolute;
top: 50%;
right: 20px;
transform: translate(0,-50%);
}
}
.body{
padding: 15px;
}
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<el-menu default-active="1" class="vertical_menu" theme="dark" :router="true" :unique-opened="true" :default-active="currentPath" style="overflow: auto">
<div class="logo">
<img src="../assets/logo.svg" alt="oj admin" />
</div>
<el-submenu index="general">
<template slot="title"><i class="el-icon-menu"></i>General</template>
<el-menu-item index="/user">User</el-menu-item>
<el-menu-item index="/announcement">Announcement</el-menu-item>
<el-menu-item index="/conf">System Config</el-menu-item>
<el-menu-item index="/judge-server">Judge Server</el-menu-item>
</el-submenu>
<el-submenu index="problem">
<template slot="title"><i class="el-icon-document"></i>Problem</template>
<el-menu-item index="/problem/create">Create Problem</el-menu-item>
<el-menu-item index="/problems">Problem List</el-menu-item>
</el-submenu>
<el-submenu index="contest">
<template slot="title"><i class="el-icon-document"></i>Contest</template>
<el-menu-item index="/contest">Contest List</el-menu-item>
<el-menu-item index="/contest/create">Create Contest</el-menu-item>
</el-submenu>
</el-menu>
</template>
<script>
export default{
name: 'SideMenu',
data () {
return {
currentPath: ''
}
},
mounted () {
this.currentPath = this.$route.path
}
}
</script>
<style scoped lang="less">
.vertical_menu{
width: 200px;
height: 100%;
position: fixed!important;
top: 0;
bottom: 0;
left: 0;
.logo{
margin: 20px 0;
text-align: center;
img{
background-color: #fff;
border-radius: 50%;
border: 3px solid #fff;
width: 75px;
height: 75px;
}
}
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<textarea ref="editor"></textarea>
</template>
<script>
import Simditor from 'simditor'
import 'simditor/styles/simditor.css'
import 'simditor-markdown'
import 'simditor-markdown/styles/simditor-markdown.css'
export default {
name: 'Simditor',
props: {
toolbar: {
type: Array,
default: () => ['title', 'bold', 'italic', 'underline', 'fontScale', 'color', 'ol', 'ul', '|', 'link', 'image', 'hr', '|', 'indent', 'outdent', 'alignment', '|', 'markdown']
},
value: {
type: String,
default: ''
}
},
data () {
return {
editor: null,
currentValue: this.value
}
},
mounted () {
Simditor.locale = 'en-US'
this.editor = new Simditor({
textarea: this.$refs.editor,
placeholder: this.placeholder,
toolbar: this.toolbar,
pasteImage: true,
markdown: true
})
this.editor.on('decorate', (e, src) => {
this.currentValue = this.editor.getValue()
})
let simditorBody = this.$el.parentNode.querySelector('.simditor-body')
if (simditorBody !== undefined) {
simditorBody.oninput = () => {
this.currentValue = this.editor.getValue()
}
}
this.editor.setValue(this.value)
},
watch: {
'value' (val) {
if (this.currentValue !== val) {
this.currentValue = val
this.editor.setValue(val)
}
},
'currentValue' (newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit('change', newVal)
this.$emit('input', newVal)
}
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

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

View File

@ -0,0 +1,8 @@
<template>
<el-button>Cancel</el-button>
</template>
<script>
export default{
name: 'Cancel'
}
</script>

View File

@ -0,0 +1,25 @@
<template>
<div style="display: inline-block;">
<el-tooltip class="item" effect="dark" :content="name" placement="top">
<el-button type="primary" :plain="true" size="small">
<i :class="'fa fa-' + icon" aria-hidden="true"></i>
</el-button>
</el-tooltip>
</div>
</template>
<script>
export default {
name: 'IconBtn',
props: {
name: {
type: String,
required: true
},
icon: {
type: String,
required: true
}
}
}
</script>

View File

@ -0,0 +1,8 @@
<template>
<el-button type="primary">Save</el-button>
</template>
<script>
export default{
name: 'Save'
}
</script>

11
oj/src/filters.js Normal file
View File

@ -0,0 +1,11 @@
import moment from 'moment'
// 友好显示时间
export function fromNow (time) {
return moment(time * 3).fromNow()
}
// 只显示日期
export function onlyDate (time) {
const d = new Date(time * 1000)
return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
}

View File

@ -1,15 +1,128 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import Element from 'element-ui'
import 'element-ui/lib/theme-default/index.css'
import VueRouter from 'vue-router'
import locale from 'element-ui/lib/locale/lang/en'
Vue.config.productionTip = false
import * as filters from './filters.js'
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
import Panel from 'components/Panel.vue'
import IconBtn from 'components/btn/IconBtn.vue'
import Save from 'components/btn/Save.vue'
import Cancel from 'components/btn/Cancel.vue'
// register global utility filters.
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key])
})
Vue.use(Element, { locale })
Vue.use(VueRouter)
// Vue.use(VueI18n)
Vue.component(IconBtn.name, IconBtn)
Vue.component(Panel.name, Panel)
Vue.component(Save.name, Save)
Vue.component(Cancel.name, Cancel)
// 引入 view 组件
import { Announcement, User, Conf, JudgeServer, Problem, Contest, ContestList,
ContestAnnouncement, ProblemList } from './views'
const router = new VueRouter({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: [
{
path: '/announcement',
name: 'announcement',
component: Announcement
},
{
path: '/user',
name: 'user',
component: User
},
{
path: '/conf',
name: 'conf',
component: Conf
},
{
path: '/judge-server',
name: 'judge-server',
component: JudgeServer
},
{
path: '/problems',
name: 'problem-list',
component: ProblemList
},
{
path: '/problem/create',
name: 'create-problem',
component: Problem
},
{
path: '/problem/edit/:problemId',
name: 'edit-problem',
component: Problem
},
{
path: '/contest/create',
name: 'create-contest',
component: Contest
},
{
path: '/contest',
name: 'contest-list',
component: ContestList
},
{
path: '/contest/:contestId/edit',
name: 'edit-contest',
component: Contest
},
{
path: '/contest/:contestId/announcement',
name: 'contest-announcement',
component: ContestAnnouncement
},
{
path: '/contest/:contestId/problems',
name: 'contest-problem-list',
component: ProblemList
},
{
path: '/contest/:contestId/problem/create',
name: 'create-contest-problem',
component: Problem
},
{
path: '/contest/:contestId/problem/:problemId/edit',
name: 'edit-contest-problem',
component: Problem
},
{
path: '*', redirect: '/announcement'
}
]
})
Vue.prototype.$error = (msg) => {
Vue.prototype.$message({'message': msg, 'type': 'error'})
}
Vue.prototype.$alert = (msg) => {
Vue.prototype.$message({'message': msg, 'type': 'info'})
}
Vue.prototype.$success = (msg) => {
if (!msg) {
Vue.prototype.$message({'message': 'Succeeded', 'type': 'success'})
} else {
Vue.prototype.$message({'message': msg, 'type': 'success'})
}
}
new Vue(Vue.util.extend({ router }, App)).$mount('#app')

View File

@ -1,15 +0,0 @@
import Vue from 'vue'
import Router from 'vue-router'
import Hello from '@/components/Hello'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Hello',
component: Hello
}
]
})

5
oj/src/utils.js Normal file
View File

@ -0,0 +1,5 @@
import moment from 'moment'
export function backendDatetimeToISOFormat (dt) {
return moment(dt, 'YYYY-M-DD HH:mm:ss zz').format()
}

View File

@ -0,0 +1,158 @@
<template>
<div class="announcement view">
<Panel title="Contest Announcement">
<div class="list">
<el-table
v-loading="loading"
element-loading-text="loading"
ref="table"
:data="announcementList"
style="width: 100%">
<el-table-column
prop="title"
label="Title">
</el-table-column>
<el-table-column
prop="created_by.username"
label="Author">
</el-table-column>
<el-table-column
prop="create_time"
label="CreateTime">
</el-table-column>
<el-table-column
inline-template
fixed="right"
label="Option">
<span>
<el-button type="text" size="small" @click="deleteAnnouncement(row.id)">Delete</el-button>
</span>
</el-table-column>
</el-table>
<div class="option">
<el-button type="primary" size="small" @click="openAnnouncementDialog()" icon="plus">Create</el-button>
</div>
</div>
</Panel>
<!--对话框-->
<el-dialog title="Create Contest Announcement" @open="openAnnouncementDialog" v-model="showEditAnnouncementDialog">
<el-form label-position="top">
<el-form-item label="Title" required>
<el-input
v-model="announcement.title"
placeholder="Title" class="title-input">
</el-input>
</el-form-item>
<el-form-item label="Content" required>
<Simditor v-model="announcement.content"></Simditor>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<cancel @click.native="showEditAnnouncementDialog = false"></cancel>
<save type="primary" @click.native="saveAnnouncement()">Submit</save>
</span>
</el-dialog>
</div>
</template>
<script>
import Simditor from '../../components/Simditor.vue'
import api from '../../api.js'
export default {
name: 'Announcement',
components: {
Simditor
},
data () {
return {
showEditAnnouncementDialog: false,
//
announcementList: [
],
announcement: {
title: '',
content: '',
contest_id: ''
},
// loading
loading: true
}
},
methods: {
getContestAnnouncementList () {
this.loading = true
api.getContestAnnouncementList(this.$route.params.contestId).then(res => {
this.announcementList = res.data.data
this.loading = false
}).catch(() => {
this.loading = false
})
},
openAnnouncementDialog () {
// todo
// bug
setTimeout(() => {
if (document.createEvent) {
let event = document.createEvent('HTMLEvents')
event.initEvent('resize', true, true)
window.dispatchEvent(event)
} else if (document.createEventObject) {
window.fireEvent('onresize')
}
}, 0)
this.showEditAnnouncementDialog = true
},
//
saveAnnouncement () {
this.announcement.contest_id = this.$route.params.contestId
api.createContestAnnouncement(this.announcement).then(() => {
this.showEditAnnouncementDialog = false
this.announcement.title = this.announcement.content = ''
this.getContestAnnouncementList()
}).catch(() => {
this.showEditAnnouncementDialog = false
})
},
//
deleteAnnouncement (announcementId) {
this.$confirm('Are you sure you want to delete this announcement?', 'Warning', {
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.loading = true
api.deleteContestAnnouncement(announcementId).then(res => {
this.getContestAnnouncementList()
})
}).catch(() => {})
}
},
mounted () {
this.getContestAnnouncementList()
}
}
</script>
<style lang="less" scoped>
.announcement {
.option{
border: 1px solid #e0e6ed;
border-top: none;
padding: 10px;
background-color: #fff;
position: relative;
button{
margin-right: 10px;
}
}
}
.title-input {
margin-bottom: 20px;
}
.visible-box{
margin-top: 10px;
width: 205px;
float: left;
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<div class="view">
<Panel :title="title">
<el-form label-position="top">
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="Title" required>
<el-input v-model="contest.title" placeholder="Tittle"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="Description" required>
<Simditor v-model="contest.description"></Simditor>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Start Time" required>
<el-date-picker
v-model="contest.start_time"
type="datetime"
placeholder="Start Time">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="End Time" required>
<el-date-picker
v-model="contest.end_time"
type="datetime"
placeholder="End Time">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Password">
<el-input v-model="contest.password" placeholder="Contest Password"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Contest Rule Type">
<el-radio class="radio" v-model="contest.rule_type" label="ACM" :disabled="disableRuleType">ACM</el-radio>
<el-radio class="radio" v-model="contest.rule_type" label="OI" :disabled="disableRuleType">OI</el-radio>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Real Time Rank">
<el-switch
v-model="contest.real_time_rank"
on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Status">
<el-switch
v-model="contest.visible"
on-text=""
off-text="">
</el-switch>
</el-form-item>
</el-col>
</el-row>
</el-form>
<save @click.native="saveContest"></save>
</Panel>
</div>
</template>
<script>
import api from '../../api.js'
import Simditor from '../../components/Simditor.vue'
import { backendDatetimeToISOFormat } from '../../utils'
export default{
name: 'CreateContest',
components: {
Simditor
},
data () {
return {
title: 'Create Contest',
disableRuleType: false,
contest: {
title: '',
description: '',
start_time: '',
end_time: '',
rule_type: 'ACM',
password: '',
real_time_rank: true,
visible: true
}
}
},
methods: {
saveContest () {
let funcName = this.$route.name === 'edit-contest' ? 'editContest' : 'createContest'
api[funcName](this.contest).then(res => {
this.$router.push({name: 'contest-list', query: {refresh: 'true'}})
}).catch(() => {})
}
},
mounted () {
if (this.$route.name === 'edit-contest') {
this.title = 'Edit Contest'
this.disableRuleType = true
api.getContest(this.$route.params.contestId).then(res => {
let data = res.data.data
data.start_time = backendDatetimeToISOFormat(data.start_time)
data.end_time = backendDatetimeToISOFormat(data.end_time)
this.contest = data
}).catch(() => {
})
}
}
}
</script>

View File

@ -0,0 +1,143 @@
<template>
<div class="view">
<Panel title="Contest List">
<div slot="header">
<el-input
v-model="keyword"
icon="search"
placeholder="Keywords">
</el-input>
</div>
<el-table
v-loading="loading"
element-loading-text="loading"
ref="table"
:data="contestList"
style="width: 100%">
<el-table-column type="expand">
<template scope="props">
<div>
<el-tag :type="props.row.visible ? 'success' : 'danger'">{{props.row.visible ? 'Visible' : 'Invisible'}}</el-tag>
<el-tag :type="props.row.contest_status === 'Public' ? 'success' : 'primary'">{{ props.row.contest_type}}</el-tag>
<el-tag :type="props.row.status === 'Ended' ? 'gray' : props.row.status === 'Underway' ? 'success' : 'primary'">{{ props.row.status }}</el-tag>
</div>
<p>
Create Time: {{ props.row.create_time }}
</p>
</template>
</el-table-column>
<el-table-column
prop="id"
label="ID">
</el-table-column>
<el-table-column
prop="title"
label="Title">
</el-table-column>
<el-table-column
prop="start_time"
label="Start Time">
</el-table-column>
<el-table-column
prop="end_time"
label="End Time">
</el-table-column>
<el-table-column
prop="created_by.username"
label="Author">
</el-table-column>
<el-table-column
inline-template
:context="_self"
fixed="right"
label="Operation">
<div>
<icon-btn name="Edit" icon="edit" @click.native="goEdit(row.id)"></icon-btn>
<icon-btn name="Problem" icon="list-ol" @click.native="goContestProblemList(row.id)"></icon-btn>
<icon-btn name="Announcement" icon="info-circle" @click.native="goContestAnnouncement(row.id)"></icon-btn>
</div>
</el-table-column>
</el-table>
<div class="option">
<el-pagination
class="page"
layout="prev, pager, next"
@current-change="currentChange"
:page-size="pageSize"
:total="total">
</el-pagination>
</div>
</Panel>
</div>
</template>
<script>
import api from '../../api.js'
export default{
name: 'ContestList',
data () {
return {
pageSize: 5,
total: 0,
contestList: [],
keyword: '',
loading: false,
currentPage: 1
}
},
mounted () {
this.getContestList(this.currentPage)
},
methods: {
//
currentChange (page) {
this.currentPage = page
this.getContestList(page)
},
getContestList (page) {
this.loading = true
api.getContestList((page - 1) * this.pageSize, this.pageSize, this.keyword).then(res => {
this.loading = false
this.total = res.data.data.total
this.contestList = res.data.data.results
}, res => {
this.loading = false
})
},
goEdit (contestId) {
this.$router.push({name: 'edit-contest', params: {contestId}})
},
goContestAnnouncement (contestId) {
this.$router.push({name: 'contest-announcement', params: {contestId}})
},
goContestProblemList (contestId) {
this.$router.push({name: 'contest-problem-list', params: {contestId}})
}
},
watch: {
'keyword' () {
this.currentChange(1)
}
}
}
</script>
<style scoped lang="less">
.option{
border: 1px solid #e0e6ed;
border-top: none;
padding: 8px;
background-color: #fff;
position: relative;
height: 50px;
button{
margin-right: 10px;
}
>.page{
position: absolute;
right: 20px;
top: 10px;
}
}
</style>

View File

@ -0,0 +1,232 @@
<template>
<div class="announcement view">
<Panel title="Announcement">
<div class="list">
<el-table
v-loading="loading"
element-loading-text="loading"
ref="table"
:data="announcementList"
style="width: 100%">
<el-table-column
prop="id"
label="ID">
</el-table-column>
<el-table-column
prop="title"
label="Title">
</el-table-column>
<el-table-column
prop="create_time"
label="CreateTime">
</el-table-column>
<el-table-column
prop="last_update_time"
label="LastUpdateTime">
</el-table-column>
<el-table-column
prop="created_by.username"
label="Author">
</el-table-column>
<el-table-column
prop="visible"
label="Status"
inline-template>
<el-tag :type="row.visible ? 'success' : 'danger'">{{row.visible ? 'Visible' : 'Invisible'}}</el-tag>
</el-table-column>
<el-table-column
inline-template
fixed="right"
label="Option">
<div>
<icon-btn name="Edit" icon="edit" @click.native="openAnnouncementDialog(row.id)"></icon-btn>
<icon-btn name="Delete" icon="trash" @click.native="deleteAnnouncement(row.id)"></icon-btn>
</div>
</el-table-column>
</el-table>
<div class="option">
<el-button type="primary" size="small" @click="openAnnouncementDialog(null)" icon="plus">Create</el-button>
<el-pagination
class="page"
layout="prev, pager, next"
@current-change="currentChange"
:page-size = "pageSize"
:total="total">
</el-pagination>
</div>
</div>
</Panel>
<!--对话框-->
<el-dialog :title="announcementDialogTitle" @open="onOpenEditDialog" v-model="showEditAnnouncementDialog">
<el-form label-position="top">
<el-form-item label="Title" required>
<el-input
v-model="announcement.title"
placeholder="Title" class="title-input">
</el-input>
</el-form-item>
<el-form-item label="Content" required>
<Simditor v-model="announcement.content"></Simditor>
</el-form-item>
<div class="visible-box">
<span>Status</span>
<el-switch
v-model="announcement.visible"
on-text=""
off-text="">
</el-switch>
</div>
</el-form>
<span slot="footer" class="dialog-footer">
<cancel @click.native="showEditAnnouncementDialog = false"></cancel>
<save type="primary" @click.native="saveAnnouncement()"></save>
</span>
</el-dialog>
</div>
</template>
<script>
import Simditor from '../../components/Simditor.vue'
import api from '../../api.js'
export default {
name: 'Announcement',
components: {
Simditor
},
data () {
return {
//
showEditAnnouncementDialog: false,
//
announcementList: [
],
//
pageSize: 15,
//
total: 0,
// id
currentAnnouncementId: null,
// (new | edit) model
announcement: {
title: '',
visible: true,
content: ''
},
//
announcementDialogTitle: 'Edit Announcement',
// loading
loading: true,
//
currentPage: 0
}
},
methods: {
//
currentChange (page) {
this.currentPage = page
this.getAnnouncementList((page - 1) * this.pageSize, this.pageSize)
},
getAnnouncementList (page) {
this.loading = true
api.getAnnouncementList((page - 1) * this.pageSize, this.pageSize).then(res => {
this.loading = false
this.total = res.data.data.total
this.announcementList = res.data.data.results
}, res => {
this.loading = false
})
},
//
onOpenEditDialog () {
// todo
// bug
setTimeout(() => {
if (document.createEvent) {
let event = document.createEvent('HTMLEvents')
event.initEvent('resize', true, true)
window.dispatchEvent(event)
} else if (document.createEventObject) {
window.fireEvent('onresize')
}
}, 0)
},
//
saveAnnouncement () {
if (this.currentAnnouncementId) {
api.modifyAnnouncement(this.currentAnnouncementId, this.announcement.title, this.announcement.content, this.announcement.visible).then(res => {
this.showEditAnnouncementDialog = false
this.getAnnouncementList(this.currentPage - 1)
}).catch(() => {})
} else {
api.createAnnouncement(this.announcement.title, this.announcement.content, this.announcement.visible).then(res => {
this.showEditAnnouncementDialog = false
this.getAnnouncementList(this.currentPage)
}).catch(() => {})
}
},
//
deleteAnnouncement (announcementId) {
this.$confirm('Are you sure you want to delete this announcement?', 'Warning', {
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.loading = true
api.deleteAnnouncement(announcementId).then(res => {
this.getAnnouncementList(this.currentPage)
})
}).catch(() => {})
},
openAnnouncementDialog (id) {
this.showEditAnnouncementDialog = true
if (id !== null) {
this.currentAnnouncementId = id
this.announcementDialogTitle = 'Edit Announcement'
this.announcementList.find(item => {
if (item.id === this.currentAnnouncementId) {
this.announcement.title = item.title
this.announcement.visible = item.visible
this.announcement.content = item.content
}
})
} else {
this.announcementDialogTitle = 'Create Announcement'
this.announcement.title = ''
this.announcement.visible = true
this.announcement.content = ''
}
}
},
mounted () {
this.getAnnouncementList(1)
}
}
</script>
<style lang="less" scoped>
.announcement {
.option{
border: 1px solid #e0e6ed;
border-top: none;
padding: 10px;
background-color: #fff;
position: relative;
button{
margin-right: 10px;
}
>.page{
position: absolute;
right: 20px;
top: 10px;
}
}
}
.title-input {
margin-bottom: 20px;
}
.visible-box{
margin-top: 10px;
width: 205px;
float: left;
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<div class="view">
<Panel title="SMTP Config">
<el-form label-position="left" label-width="70px" :model="smtp">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="Server" required>
<el-input v-model="smtp.server" placeholder="SMTP Server Address"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Port" required>
<el-input type="number" v-model="smtp.port" placeholder="SMTP Server Port"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Email" required>
<el-input type="email" v-model="smtp.email" placeholder="Account Used To Send Email"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Password" label-width="90px" required>
<el-input v-model="smtp.password" type="password" placeholder="SMTP Server Password"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="TLS">
<el-switch
v-model="smtp.tls"
on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</el-form-item>
</el-col>
</el-row>
</el-form>
<save @click.native="saveSMTPConfig"></save>
</Panel>
<Panel title="Website Config">
<el-form label-position="left" label-width="100px" ref="form" :model="websiteConfig">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="Base Url" required>
<el-input v-model="websiteConfig.base_url" placeholder="Website Base Url"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Name" required>
<el-input v-model="websiteConfig.name" placeholder="Website Name"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Shortcut" required>
<el-input v-model="websiteConfig.name_shortcut" placeholder="Website Name Shortcut"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="Footer" required>
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4}" v-model="websiteConfig.footer"
placeholder="Website Footer HTML"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-col :span="12">
<el-form-item label="Allow Register" label-width="200px">
<el-switch
v-model="websiteConfig.allow_register"
on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Submission List Show All" label-width="200px">
<el-switch
v-model="websiteConfig.submission_list_show_all"
on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</el-form-item>
</el-col>
</el-col>
</el-row>
</el-form>
<save @click.native="saveWebsiteConfig"></save>
</Panel>
</div>
</template>
<script>
import api from '../../api.js'
export default{
name: 'Conf',
data () {
return {
init: false,
smtp: {
server: 'smtp.example.com',
port: 25,
password: '',
email: 'email@example.com',
tls: true
},
websiteConfig: {}
}
},
mounted () {
api.getSMTPConfig().then(res => {
if (res.data.data) {
this.smtp = res.data.data
} else {
this.init = true
this.$alert('Please setup SMTP config at first')
}
})
api.getWebsiteConfig().then(res => {
this.websiteConfig = res.data.data
}).catch(() => {})
},
methods: {
saveSMTPConfig () {
if (!this.init) {
api.editSMTPConfig(this.smtp)
} else {
api.createSMTPConfig(this.smtp)
}
},
saveWebsiteConfig () {
api.editWebsiteConfig(this.websiteConfig).then(() => {}).catch(() => {})
}
}
}
</script>

View File

@ -0,0 +1,107 @@
<template>
<div class="view">
<Panel title="Judge Server Token">
<el-input v-model="token" style="width: 200px"></el-input>
</Panel>
<Panel title="Judge Server">
<el-table
:data="servers"
:default-expand-all="true"
border>
<el-table-column
type="expand">
<template scope="props">
<p>IP: <el-tag type="success">{{ props.row.ip }}</el-tag>&nbsp;&nbsp;
Judger Version: <el-tag type="success">{{ props.row.judger_version }}</el-tag>
</p>
<p>Service URL: <code>{{ props.row.service_url }}</code></p>
<p>Last Heartbeat: {{ props.row.last_heartbeat}}&nbsp;&nbsp;Create Time: {{ props.row.create_time }}</p>
</template>
</el-table-column>
<el-table-column
prop="status"
label="Status">
<template scope="scope">
<el-tag
:type="scope.row.status === 'normal' ? 'success' : 'danger'">
{{ scope.row.status === 'normal' ? 'Normal' : 'Abnormal' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="hostname"
label="Hostname">
</el-table-column>
<el-table-column
prop="task_number"
label="Task Number">
</el-table-column>
<el-table-column
prop="cpu_core"
label="CPU Core">
</el-table-column>
<el-table-column
prop="cpu_usage"
label="CPU Usage">
<template scope="scope">{{ scope.row.cpu_usage }}%</template>
</el-table-column>
<el-table-column
prop="memory_usage"
label="Memory Usage">
<template scope="scope">{{ scope.row.memory_usage }}%</template>
</el-table-column>
<el-table-column
fixed="right"
label="option">
<template scope="scope">
<icon-btn name="Delete" icon="trash" @click.native="deleteJudgeServer(scope.row.hostname)"></icon-btn>
</template>
</el-table-column>
</el-table>
</Panel>
</div>
</template>
<script>
import api from '../../api.js'
export default{
name: 'JudgeServer',
data () {
return {
servers: [],
token: '',
intervalId: -1
}
},
mounted () {
this.refreshJudgeServerList()
this.intervalId = setInterval(() => {
this.refreshJudgeServerList()
}, 5000)
},
methods: {
refreshJudgeServerList () {
api.getJudgeServer().then(res => {
this.servers = res.data.data.servers
this.token = res.data.data.token
})
},
deleteJudgeServer (hostname) {
this.$confirm('If you delete this judge server, it can\'t be used until next heartbeat', 'Warning', {
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
api.deleteJudgeServer(hostname).then(res =>
this.refreshJudgeServerList()
)
}).catch(() => {})
}
},
beforeRouteLeave (to, from, next) {
clearInterval(this.intervalId)
next()
}
}
</script>

View File

@ -0,0 +1,239 @@
<template>
<div class="view">
<Panel title="User List">
<div slot="header">
<el-input
v-model="keyword"
icon="search"
placeholder="Keywords">
</el-input>
</div>
<el-table
v-loading="loading"
element-loading-text="loading"
ref="table"
:data="userList"
style="width: 100%">
<el-table-column
prop="id"
label="ID">
</el-table-column>
<el-table-column
prop="username"
label="Userame">
</el-table-column>
<el-table-column
prop="create_time"
label="Create Time">
</el-table-column>
<el-table-column
prop="last_login"
label="Last Login">
</el-table-column>
<el-table-column
prop="real_name"
label="Real Name">
</el-table-column>
<el-table-column
prop="email"
label="Email">
</el-table-column>
<el-table-column
prop="admin_type"
label="User Type">
<template scope="scope">
{{ scope.row.admin_type }}
</template>
</el-table-column>
<el-table-column
inline-template
fixed="right"
label="Option">
<icon-btn name="Edit" icon="edit" @click.native="openUserDialog(row.id)"></icon-btn>
</el-table-column>
</el-table>
<div class="option">
<el-pagination
class="page"
layout="prev, pager, next"
@current-change="currentChange"
:page-size="pageSize"
:total="total">
</el-pagination>
</div>
</Panel>
<!--对话框-->
<el-dialog title="User" v-model="showUserDialog">
<el-form :model="user" label-width="120px" label-position="left">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="Username" required>
<el-input v-model="user.username"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Real Name" required>
<el-input v-model="user.real_name"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Email" required>
<el-input v-model="user.email"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="New Password">
<el-input v-model="user.password"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="User Type">
<el-select v-model="user.admin_type">
<el-option label="Regular User" value="Regular User"></el-option>
<el-option label="Admin" value="Admin"></el-option>
<el-option label="Super Admin" value="Super Admin"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Problem Permission">
<el-select v-model="user.problem_permission" :disabled="user.admin_type!=='Admin'">
<el-option label="None" value="None"></el-option>
<el-option label="Own" value="Own"></el-option>
<el-option label="All" value="All"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Two Factor Auth">
<el-switch
v-model="user.two_factor_auth"
on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Open Api">
<el-switch
v-model="user.open_api"
on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Is Disabled">
<el-switch
v-model="user.is_disabled"
on-text=""
off-text="">
</el-switch>
</el-form-item>
</el-col>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<cancel @click.native="showUserDialog = false">Cancel</cancel>
<save @click.native="saveUser()"></save>
</span>
</el-dialog>
</div>
</template>
<script>
import api from '../../api.js'
export default{
name: 'User',
data () {
return {
//
pageSize: 5,
//
total: 0,
//
userList: [],
//
keyword: '',
//
showUserDialog: false,
// model
user: {},
// loading
loading: false,
//
currentPage: 0
}
},
mounted () {
this.getUserList(1)
},
methods: {
//
currentChange (page) {
this.currentPage = page
this.getUserList(page)
},
//
saveUser () {
api.editUser(this.user).then(res => {
//
this.getUserList(this.currentPage)
}).then(() => {
this.showUserDialog = false
}).catch(() => {})
},
//
openUserDialog (id) {
this.showUserDialog = true
api.getUser(id).then(res => {
this.user = res.data.data
this.user.password = ''
})
},
//
getUserList (page) {
this.loading = true
api.getUserList((page - 1) * this.pageSize, this.pageSize, this.keyword).then(res => {
this.loading = false
this.total = res.data.data.total
this.userList = res.data.data.results
}, res => {
this.loading = false
})
}
},
watch: {
'keyword' () {
this.currentChange(1)
},
'user.admin_type' () {
if (this.user.admin_type === 'Super Admin') {
this.user.problem_permission = 'All'
} else if (this.user.admin_type === 'Regular User') {
this.user.problem_permission = 'None'
}
}
}
}
</script>
<style scoped lang="less">
.option{
border: 1px solid #e0e6ed;
border-top: none;
padding: 8px;
background-color: #fff;
position: relative;
height: 50px;
button{
margin-right: 10px;
}
>.page{
position: absolute;
right: 20px;
top: 10px;
}
}
</style>

14
oj/src/views/index.js Normal file
View File

@ -0,0 +1,14 @@
import Announcement from './general/Announcement.vue'
import User from './general/User.vue'
import Conf from './general/Conf.vue'
import JudgeServer from './general/JudgeServer.vue'
import Problem from './problem/Problem.vue'
import ProblemList from './problem/ProblemList.vue'
import ContestList from './contest/ContestList.vue'
import Contest from './contest/Contest.vue'
import ContestAnnouncement from './contest/Announcement.vue'
export {
Announcement, User, Conf, JudgeServer, Problem, ProblemList, Contest,
ContestList, ContestAnnouncement
}

View File

@ -0,0 +1,559 @@
<template>
<div class="problem">
<Panel title="Import Problem" v-if="mode == 'add'">
<el-upload
action="/api/admin/test_case/upload"
name="file"
:data="{spj: problem.spj}"
:show-upload-list="false"
:on-success="uploadSucceeded"
:on-error="uploadFailed">
<el-button size="small" type="primary">Choose File</el-button>
</el-upload>
</Panel>
<Panel :title="title">
<el-form ref="form" :model="problem" :rules="rules" label-position="top" label-width="70px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item prop="_id" label="Display ID" :required="this.routeName === 'create-contest-problem' || this.routeName === 'edit-contet-problem'">
<el-input placeholder="Display ID" v-model="problem._id"></el-input>
</el-form-item>
</el-col>
<el-col :span="18">
<el-form-item prop="title" label="Title" required>
<el-input placeholder="Title" v-model="problem.title"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item prop="description" label="Description" required>
<Simditor placeholder="" v-model="problem.description"></Simditor>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item required prop="input_description" label="Input Description" required>
<el-input
type="textarea"
:autosize="{ minRows: 3, maxRows: 8}"
placeholder="Input Description"
v-model="problem.input_description">
</el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item required prop="output_description" label="Output Description" required>
<el-input
type="textarea"
:autosize="{ minRows: 3, maxRows: 8}"
placeholder="Output Description"
v-model="problem.output_description">
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="Time Limit" required>
<el-input type="Number" placeholder="Time Limit" v-model="problem.time_limit"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Memory limit" required>
<el-input type="Number" placeholder="Memory Limit" v-model="problem.memory_limit"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Difficulty">
<el-select class="difficulty-select" size="small" placeholder="Difficulty" v-model="problem.difficulty">
<el-option label="Low" value="Low"></el-option>
<el-option label="Mid" value="Mid"></el-option>
<el-option label="High" value="High"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="Visible">
<el-switch
v-model="problem.visible"
on-text=""
off-text="">
</el-switch>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Tag" :error="error.tags" required>
<span class="tags">
<el-tag
v-for="tag in problem.tags"
:closable="true"
:close-transition="false"
type="success"
@close="closeTag(tag)"
>{{tag}}</el-tag>
</span>
<el-autocomplete
v-if="inputVisible"
size="mini"
class="input-new-tag"
v-model="tagInput"
:trigger-on-focus="false"
@keyup.enter.native="addTag"
@select="addTag"
:fetch-suggestions="querySearch">
</el-autocomplete>
<el-button class="button-new-tag" v-else size="small" @click="inputVisible = true">+ New Tag</el-button>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Languages" :error="error.languages" required>
<el-checkbox-group v-model="problem.languages">
<el-tooltip class="spj-radio" v-for="lang in allLanguage.languages" effect="dark" :content="lang.description" placement="top-start">
<el-checkbox :label="lang.name"></el-checkbox>
</el-tooltip>
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<div>
<el-form-item v-for="(sample, index) in problem.samples">
<Accordion :title="'Sample' + (index + 1)">
<el-button type="warning" size="small" icon="delete" slot="header" @click="deleteSample(index)">Delete</el-button>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="Input Samples" required>
<el-input
:rows="5"
type="textarea"
placeholder="Input Samples"
v-model="sample.input">
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Output Samples" required>
<el-input
:rows="5"
type="textarea"
placeholder="Output Samples"
v-model="sample.output">
</el-input>
</el-form-item>
</el-col>
</el-row>
</Accordion>
</el-form-item>
</div>
<div class="add-sample-btn">
<button type="button" class="add-samples" @click="addSample()"><i class="el-icon-plus"></i>Add Samples</button>
</div>
<el-form-item label="Code Template">
<el-row>
<el-col :span="24" v-for="(v, k) in template">
<el-form-item>
<el-checkbox v-model="v.checked">{{ k }}</el-checkbox>
<div v-if="v.checked">
<code-mirror v-model="v.code" :mode="v.mode"></code-mirror>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="Special Judge" :error="error.spj">
<el-row :gutter="20">
<el-col :span="12">
<el-checkbox v-model="problem.spj" @click.native.prevent="switchSpj()">Use Special Judge</el-checkbox>
</el-col>
<el-col v-if="problem.spj" :span="12">
<el-form-item label="Special Judge Language">
<el-radio-group v-model="problem.spj_language">
<el-tooltip class="spj-radio" v-for="lang in allLanguage.spj_languages" effect="dark" :content="lang.description" placement="top-start">
<el-radio :label="lang.name">{{ lang.name }}</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="problem.spj" label="Special Judge Code">
<code-mirror v-model="problem.spj_code" :mode="spjMode"></code-mirror>
</el-form-item>
</el-form-item>
<el-row :gutter="20">
<el-col :span="4">
<el-form-item label="TestCase" :error="error.testcase">
<el-upload
action="/api/admin/test_case/upload"
name="file"
:data="{spj: problem.spj}"
:show-upload-list="false"
:on-success="uploadSucceeded"
:on-error="uploadFailed">
<el-button size="small" type="primary">Choose File</el-button>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Type">
<el-radio-group v-model="problem.rule_type" :disabled="disableRuleType">
<el-radio label="ACM">ACM</el-radio>
<el-radio label="OI">OI</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-table
:data="problem.test_case_score"
style="width: 100%">
<el-table-column
prop="input_name"
label="Input">
</el-table-column>
<el-table-column
prop="output_name"
label="Output">
</el-table-column>
<el-table-column
prop="score"
label="Score">
<template scope="scope">
<el-input
size="small"
placeholder="Score"
v-model="scope.row.score"
:disabled="problem.rule_type !== 'OI'">
</el-input>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
<el-form-item style="margin-top: 20px" label="Hint">
<Simditor v-model="problem.hint" placeholder=""></Simditor>
</el-form-item>
<el-form-item label="Source">
<el-input placeholder="Source" v-model="problem.source"></el-input>
</el-form-item>
<save @click.native="submit()">Save</save>
</el-form>
</Panel>
</div>
</template>
<script>
import Simditor from '../../components/Simditor'
import Accordion from '../../components/Accordion'
import CodeMirror from '../../components/CodeMirror'
import api from '../../api'
export default{
name: 'Problem',
components: {
Simditor,
Accordion,
CodeMirror
},
data () {
return {
rules: {
title: { required: true, message: 'Title is required', trigger: 'blur' },
input_description: { required: true, message: 'Input Description is required', trigger: 'blur' },
output_description: { required: true, message: 'Output Description is required', trigger: 'blur' }
},
mode: '',
problem: {
languages: []
},
reProblem: {
languages: []
},
testCaseUploaded: false,
allLanguage: {},
inputVisible: false,
tagInput: '',
template: {},
title: '',
spjMode: '',
disableRuleType: false,
routeName: '',
error: {
tags: '',
spj: '',
languages: '',
testCase: ''
}
}
},
mounted () {
this.routeName = this.$route.name
if (this.routeName === 'edit-problem' || this.routeName === 'edit-contest-problem') {
this.mode = 'edit'
} else {
this.mode = 'add'
}
api.getLanguages().then(res => {
this.problem = this.reProblem = {
_id: '',
title: '',
description: '',
input_description: '',
output_description: '',
time_limit: 1000,
memory_limit: 256,
difficulty: 'Low',
visible: true,
tags: [],
languages: [],
template: {},
samples: [{input: '', output: ''}],
spj: false,
spj_language: '',
spj_code: '',
test_case_id: '',
test_case_score: [],
rule_type: 'ACM',
hint: '',
source: ''
}
this.problem.spj_language = 'C'
let allLanguage = res.data.data
this.allLanguage = allLanguage
// get problem after getting languages list to avoid find undefined value in `watch problem.languages`
if (this.mode === 'edit') {
let funcName = {'edit-problem': 'getProblem', 'edit-contest-problem': 'getContestProblem'}[this.routeName]
this.title = 'Edit Problem'
api[funcName](this.$route.params.problemId).then(problemRes => {
let data = problemRes.data.data
if (!data.spj_code) {
data.spj_code = ''
}
data.spj_language = data.spj_language || 'C'
this.problem = data
this.testCaseUploaded = true
if (this.routeName === 'edit-contest-problem') {
this.problem.contest_id = this.$route.params.contestId
this.disableRuleType = true
}
})
} else {
this.title = 'Add Problem'
for (let item of allLanguage.languages) {
this.problem.languages.push(item.name)
}
if (this.routeName === 'create-contest-problem') {
this.problem.contest_id = this.$route.params.contestId
this.disableRuleType = true
}
}
})
},
watch: {
'$route' () {
this.$refs.form.resetFields()
this.problem = this.reProblem
},
'problem.languages' (newVal) {
let data = {}
for (let item of newVal) {
if (this.template[item] === undefined) {
let langConfig = this.allLanguage.languages.find(lang => {
return lang.name === item
})
if (this.problem.template[item] === undefined) {
data[item] = {checked: false, code: langConfig.config.template, mode: langConfig.content_type}
} else {
data[item] = {checked: true, code: this.problem.template[item], mode: langConfig.content_type}
}
} else {
data[item] = this.template[item]
}
}
this.template = data
},
'problem.spj_language' (newVal) {
this.spjMode = this.allLanguage.spj_languages.find(item => {
return item.name === this.problem.spj_language
}).content_type
}
},
methods: {
switchSpj () {
if (this.testCaseUploaded) {
this.$confirm('If you change problem judge method, you need to re-upload test cases', 'Warning', {
confirmButtonText: 'Yes',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.problem.spj = !this.problem.spj
this.resetTestCase()
}).catch(() => {})
} else {
this.problem.spj = !this.problem.spj
}
},
querySearch (queryString, cb) {
api.getProblemTagList().then(res => {
let tagList = []
for (let tag of res.data.data) {
tagList.push({value: tag})
}
cb(tagList)
}).catch(() => {})
},
resetTestCase () {
this.testCaseUploaded = false
this.problem.test_case_score = []
this.problem.test_case_id = ''
},
addTag () {
let inputValue = this.tagInput
if (inputValue) {
this.problem.tags.push(inputValue)
}
this.inputVisible = false
this.tagInput = ''
},
closeTag (tag) {
this.problem.tags.splice(this.problem.tags.indexOf(tag), 1)
},
addSample () {
this.problem.samples.push({input: '', output: ''})
},
deleteSample (index) {
this.problem.samples.splice(index, 1)
},
uploadSucceeded (response) {
if (response.error) {
this.$error(response.data)
return
}
let fileList = response.data.info
for (let file of fileList) {
file.score = 0
if (this.problem.spj) {
file.output_name = '-'
}
}
this.problem.test_case_score = fileList
this.testCaseUploaded = true
this.problem.test_case_id = response.data.id
},
uploadFailed () {
this.$error('Upload failed')
},
submit () {
if (!this.problem.samples.length) {
this.$error('Sample is required')
return
}
for (let sample of this.problem.samples) {
if (!sample.input || !sample.output) {
this.$error('Sample input and output is required')
return
}
}
if (!this.problem.tags.length) {
this.error.tags = 'Please add at least one tag'
this.$error(this.error.tags)
return
}
if (this.problem.spj && !this.problem.spj_code) {
this.error.spj = 'Spj code is required'
this.$error(this.error.spj)
return
}
if (!this.problem.languages.length) {
this.error.languages = 'Please choose at least one language for problem'
this.$error(this.error.languages)
return
}
if (!this.testCaseUploaded) {
this.error.testCase = 'Test case is not uploaded yet'
this.$error(this.error.testCase)
return
}
if (this.problem.rule_type === 'OI') {
for (let item of this.problem.test_case_score) {
try {
if (parseInt(item.score) <= 0) {
this.$error('Invalid test case score')
return
}
} catch (e) {
this.$error('Test case score must be an integer')
return
}
}
}
this.problem.template = {}
for (let k in this.template) {
if (this.template[k].checked) {
this.problem.template[k] = this.template[k].code
}
}
let funcName = {'create-problem': 'createProblem',
'edit-problem': 'editProblem',
'create-contest-problem': 'createContestProblem',
'edit-contest-problem': 'editContestProblem'
}[this.routeName]
api[funcName](this.problem).then(res => {
this.$router.push({name: 'problem-list'})
}).catch(() => {})
}
}
}
</script>
<style lang="less" scoped>
.problem{
.difficulty-select{
width: 120px;
}
.spj-radio{
margin-right: 15px;
}
.input-new-tag{
width: 78px;
}
.button-new-tag{
height: 24px;
line-height: 22px;
padding-top: 0;
padding-bottom: 0;
}
.tags{
.el-tag{
margin-right: 10px;
}
}
.accordion{
margin-bottom: 10px;
}
.add-samples{
width: 100%;
background-color: #fff;
border: 1px dashed #aaa;
outline: none;
cursor: pointer;
color: #666;
height: 35px;
font-size: 14px;
&:hover {
background-color: #f9fafc;
}
i{
margin-right: 10px;
}
}
.add-sample-btn {
margin-bottom: 10px;
}
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="view">
<Panel title="Problem List">
<div slot="header">
<el-input
v-model="keyword"
icon="search"
placeholder="Keywords">
</el-input>
</div>
<el-table
v-loading="loading"
element-loading-text="loading"
ref="table"
:data="problemList"
style="width: 100%">
<el-table-column
prop="id"
label="ID">
</el-table-column>
<el-table-column
prop="_id"
label="Display ID">
</el-table-column>
<el-table-column
prop="title"
label="Title">
</el-table-column>
<el-table-column
prop="created_by.username"
label="Author">
</el-table-column>
<el-table-column
prop="create_time"
label="Create Time">
</el-table-column>
<el-table-column
label="Status">
<template scope="props">
<el-tag :type="props.row.visible ? 'success' : 'danger'">{{props.row.visible ? 'Visible' : 'Invisible'}}</el-tag>
</template>
</el-table-column>
<el-table-column
inline-template
:context="_self"
fixed="right"
label="Operation"
width="180">
<div>
<icon-btn name="Edit" icon="edit" @click.native="goEdit(row.id)"></icon-btn>
<icon-btn name="Submission" icon="code"></icon-btn>
</div>
</el-table-column>
</el-table>
<div class="option">
<el-pagination
class="page"
layout="prev, pager, next"
@current-change="currentChange"
:page-size="pageSize"
:total="total">
</el-pagination>
</div>
</Panel>
</div>
</template>
<script>
import api from '../../api.js'
export default{
name: 'ProblemList',
data () {
return {
pageSize: 1,
total: 0,
problemList: [],
keyword: '',
loading: false,
currentPage: 1,
routeName: '',
contestId: ''
}
},
mounted () {
this.routeName = this.$route.name
this.contestId = this.$route.params.contestId
this.getProblemList(this.currentPage)
},
methods: {
goEdit (problemId) {
if (this.routeName === 'problem-list') {
this.$router.push({name: 'edit-problem', params: {problemId}})
} else if (this.routeName === 'contest-problem-list') {
this.$router.push({name: 'edit-contest-problem', params: {problemId: problemId, contestId: this.contestId}})
}
},
//
currentChange (page) {
this.currentPage = page
this.getProblemList(page)
},
getProblemList (page = 1) {
this.loading = true
let funcName = this.routeName === 'problem-list' ? 'getProblemList' : 'getContestProblemList'
api[funcName]((page - 1) * this.pageSize, this.pageSize, this.keyword, this.contestId).then(res => {
this.loading = false
this.total = res.data.data.total
this.problemList = res.data.data.results
}, res => {
this.loading = false
})
}
},
watch: {
'keyword' () {
this.currentChange()
}
}
}
</script>
<style scoped lang="less">
.option{
border: 1px solid #e0e6ed;
border-top: none;
padding: 8px;
background-color: #fff;
position: relative;
height: 50px;
button{
margin-right: 10px;
}
>.page{
position: absolute;
right: 20px;
top: 10px;
}
}
</style>