mirror of
https://github.com/QingdaoU/OnlineJudgeFE.git
synced 2024-12-29 16:01:51 +00:00
init oj
This commit is contained in:
parent
8d6c212c33
commit
f061a648bf
13
oj/.babelrc
13
oj/.babelrc
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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": {}
|
||||
}
|
||||
}
|
@ -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')
|
||||
})
|
||||
|
@ -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 = []
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
}
|
@ -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/)
|
||||
]
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
]
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
6
oj/config/test.env.js
Normal file
@ -0,0 +1,6 @@
|
||||
var merge = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
@ -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>
|
||||
|
@ -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
308
oj/src/api.js
Normal 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
1
oj/src/assets/logo.svg
Normal file
File diff suppressed because one or more lines are too long
76
oj/src/components/Accordion.vue
Normal file
76
oj/src/components/Accordion.vue
Normal 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>
|
66
oj/src/components/CodeMirror.vue
Normal file
66
oj/src/components/CodeMirror.vue
Normal 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>
|
@ -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>
|
71
oj/src/components/Panel.vue
Normal file
71
oj/src/components/Panel.vue
Normal 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>
|
60
oj/src/components/SideMenu.vue
Normal file
60
oj/src/components/SideMenu.vue
Normal 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>
|
66
oj/src/components/Simditor.vue
Normal file
66
oj/src/components/Simditor.vue
Normal 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>
|
18
oj/src/components/TopNav.vue
Normal file
18
oj/src/components/TopNav.vue
Normal 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>
|
8
oj/src/components/btn/Cancel.vue
Normal file
8
oj/src/components/btn/Cancel.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<el-button>Cancel</el-button>
|
||||
</template>
|
||||
<script>
|
||||
export default{
|
||||
name: 'Cancel'
|
||||
}
|
||||
</script>
|
25
oj/src/components/btn/IconBtn.vue
Normal file
25
oj/src/components/btn/IconBtn.vue
Normal 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>
|
8
oj/src/components/btn/Save.vue
Normal file
8
oj/src/components/btn/Save.vue
Normal 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
11
oj/src/filters.js
Normal 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()
|
||||
}
|
133
oj/src/main.js
133
oj/src/main.js
@ -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')
|
||||
|
@ -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
5
oj/src/utils.js
Normal file
@ -0,0 +1,5 @@
|
||||
import moment from 'moment'
|
||||
|
||||
export function backendDatetimeToISOFormat (dt) {
|
||||
return moment(dt, 'YYYY-M-DD HH:mm:ss zz').format()
|
||||
}
|
158
oj/src/views/contest/Announcement.vue
Normal file
158
oj/src/views/contest/Announcement.vue
Normal 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>
|
118
oj/src/views/contest/Contest.vue
Normal file
118
oj/src/views/contest/Contest.vue
Normal 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>
|
143
oj/src/views/contest/ContestList.vue
Normal file
143
oj/src/views/contest/ContestList.vue
Normal 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>
|
232
oj/src/views/general/Announcement.vue
Normal file
232
oj/src/views/general/Announcement.vue
Normal 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>
|
135
oj/src/views/general/Conf.vue
Normal file
135
oj/src/views/general/Conf.vue
Normal 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>
|
107
oj/src/views/general/JudgeServer.vue
Normal file
107
oj/src/views/general/JudgeServer.vue
Normal 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>
|
||||
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}} 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>
|
239
oj/src/views/general/User.vue
Normal file
239
oj/src/views/general/User.vue
Normal 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
14
oj/src/views/index.js
Normal 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
|
||||
}
|
559
oj/src/views/problem/Problem.vue
Normal file
559
oj/src/views/problem/Problem.vue
Normal 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>
|
||||
|
140
oj/src/views/problem/ProblemList.vue
Normal file
140
oj/src/views/problem/ProblemList.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user