mirror of
https://github.com/QingdaoU/OnlineJudgeFE.git
synced 2024-12-29 16:01:51 +00:00
1. 前端UI框架更改: 弃用ElementUI,改用iView
2. 更新大量陈旧组件,使用新版vue-cli的build文件 3. 加入LazyLoad机制,提升首次打开页面的速度 4. 文件结构改动,更合理更工程化。
This commit is contained in:
parent
a3049ce285
commit
8ff520fdc0
11
oj/.babelrc
11
oj/.babelrc
@ -1,5 +1,14 @@
|
||||
{
|
||||
"presets": ["es2015", "stage-2"],
|
||||
"presets": [
|
||||
["env", { "modules": false }],
|
||||
"stage-2"
|
||||
],
|
||||
"plugins": ["transform-runtime"],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": ["env", "stage-2"],
|
||||
"plugins": [ "istanbul" ]
|
||||
}
|
||||
},
|
||||
"comments": false
|
||||
}
|
||||
|
8
oj/.postcssrc.js
Normal file
8
oj/.postcssrc.js
Normal file
@ -0,0 +1,8 @@
|
||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
"plugins": {
|
||||
// to edit target browsers: use "browserlist" field in package.json
|
||||
"autoprefixer": {}
|
||||
}
|
||||
}
|
@ -1,31 +1,22 @@
|
||||
// https://github.com/shelljs/shelljs
|
||||
require('./check-versions')()
|
||||
require('shelljs/global')
|
||||
env.NODE_ENV = 'production'
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../config')
|
||||
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 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()
|
||||
|
||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||
rm('-rf', assetsPath)
|
||||
mkdir('-p', assetsPath)
|
||||
cp('-R', 'static/*', assetsPath)
|
||||
|
||||
webpack(webpackConfig, function (err, stats) {
|
||||
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
|
||||
if (err) throw err
|
||||
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,
|
||||
@ -33,5 +24,12 @@ webpack(webpackConfig, function (err, stats) {
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n')
|
||||
}) + '\n\n')
|
||||
|
||||
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'
|
||||
))
|
||||
})
|
||||
})
|
||||
|
@ -1,9 +1,9 @@
|
||||
var semver = require('semver')
|
||||
var chalk = require('chalk')
|
||||
var semver = require('semver')
|
||||
var packageConfig = require('../package.json')
|
||||
var exec = function (cmd) {
|
||||
return require('child_process')
|
||||
.execSync(cmd).toString().trim()
|
||||
var shell = require('shelljs')
|
||||
function exec (cmd) {
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
var versionRequirements = [
|
||||
@ -12,12 +12,15 @@ 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,17 +1,21 @@
|
||||
require('./check-versions')()
|
||||
|
||||
var config = require('../config')
|
||||
if (!process.env.NODE_ENV) process.env.NODE_ENV = config.dev.env
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
|
||||
}
|
||||
|
||||
var opn = require('opn')
|
||||
var path = require('path')
|
||||
var express = require('express')
|
||||
var webpack = require('webpack')
|
||||
var opn = require('opn')
|
||||
var proxyMiddleware = require('http-proxy-middleware')
|
||||
var webpackConfig = process.env.NODE_ENV === 'testing'
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
var webpackConfig = 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
|
||||
@ -21,13 +25,12 @@ var compiler = webpack(webpackConfig)
|
||||
|
||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
stats: {
|
||||
colors: true,
|
||||
chunks: false
|
||||
}
|
||||
quiet: true
|
||||
})
|
||||
|
||||
var hotMiddleware = require('webpack-hot-middleware')(compiler)
|
||||
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
|
||||
log: () => {}
|
||||
})
|
||||
// force page reload when html-webpack-plugin template changes
|
||||
compiler.plugin('compilation', function (compilation) {
|
||||
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||
@ -42,7 +45,7 @@ Object.keys(proxyTable).forEach(function (context) {
|
||||
if (typeof options === 'string') {
|
||||
options = { target: options }
|
||||
}
|
||||
app.use(proxyMiddleware(context, options))
|
||||
app.use(proxyMiddleware(options.filter || context, options))
|
||||
})
|
||||
|
||||
// handle fallback for HTML5 history API
|
||||
@ -59,12 +62,28 @@ app.use(hotMiddleware)
|
||||
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
app.use(staticPath, express.static('./static'))
|
||||
|
||||
module.exports = app.listen(port, function (err) {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
return
|
||||
}
|
||||
var uri = 'http://localhost:' + port
|
||||
console.log('Listening at ' + uri + '\n')
|
||||
opn(uri)
|
||||
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)
|
||||
}
|
||||
_resolve()
|
||||
})
|
||||
|
||||
var server = app.listen(port)
|
||||
|
||||
module.exports = {
|
||||
ready: readyPromise,
|
||||
close: () => {
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
|
@ -11,38 +11,48 @@ exports.assetsPath = function (_path) {
|
||||
|
||||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
// generate loader string to be used with extract text plugin
|
||||
function generateLoaders (loaders) {
|
||||
var sourceLoader = loaders.map(function (loader) {
|
||||
var extraParamChar
|
||||
if (/\?/.test(loader)) {
|
||||
loader = loader.replace(/\?/, '-loader?')
|
||||
extraParamChar = '&'
|
||||
} else {
|
||||
loader = loader + '-loader'
|
||||
extraParamChar = '?'
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
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('vue-style-loader', sourceLoader)
|
||||
return ExtractTextPlugin.extract({
|
||||
use: loaders,
|
||||
fallback: 'vue-style-loader'
|
||||
})
|
||||
} else {
|
||||
return ['vue-style-loader', sourceLoader].join('!')
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
|
||||
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
|
||||
return {
|
||||
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'])
|
||||
css: generateLoaders(),
|
||||
postcss: generateLoaders(),
|
||||
less: generateLoaders('less'),
|
||||
sass: generateLoaders('sass', { indentedSyntax: true }),
|
||||
scss: generateLoaders('sass'),
|
||||
stylus: generateLoaders('stylus'),
|
||||
styl: generateLoaders('stylus')
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +64,7 @@ exports.styleLoaders = function (options) {
|
||||
var loader = loaders[extension]
|
||||
output.push({
|
||||
test: new RegExp('\\.' + extension + '$'),
|
||||
loader: loader
|
||||
use: loader
|
||||
})
|
||||
}
|
||||
return output
|
||||
|
12
oj/build/vue-loader.conf.js
Normal file
12
oj/build/vue-loader.conf.js
Normal file
@ -0,0 +1,12 @@
|
||||
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,15 +1,11 @@
|
||||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var config = require('../config')
|
||||
var vueLoaderConfig = require('./vue-loader.conf')
|
||||
|
||||
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')
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '..', dir)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
@ -17,84 +13,55 @@ module.exports = {
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js'
|
||||
filename: '[name].js',
|
||||
publicPath: process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsPublicPath
|
||||
: config.dev.assetsPublicPath
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.vue'],
|
||||
fallback: [path.join(__dirname, '../node_modules')],
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue',
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components')
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
'@': resolve('src')
|
||||
}
|
||||
},
|
||||
resolveLoader: {
|
||||
fallback: [path.join(__dirname, '../node_modules')]
|
||||
},
|
||||
module: {
|
||||
preLoaders: [
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'eslint',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'eslint',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
enforce: 'pre',
|
||||
include: [resolve('src'), resolve('test')],
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
}
|
||||
],
|
||||
loaders: [
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue'
|
||||
loader: 'vue-loader',
|
||||
options: vueLoaderConfig
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json'
|
||||
loader: 'babel-loader',
|
||||
include: [resolve('src'), resolve('test')]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: 'url',
|
||||
query: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: 'url',
|
||||
query: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
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,9 +1,10 @@
|
||||
var config = require('../config')
|
||||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var config = require('../config')
|
||||
var merge = require('webpack-merge')
|
||||
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) {
|
||||
@ -12,23 +13,23 @@ Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
},
|
||||
// eval-source-map is faster for development
|
||||
devtool: '#eval-source-map',
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: '#cheap-module-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.NoErrorsPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
inject: true
|
||||
})
|
||||
}),
|
||||
new FriendlyErrorsPlugin()
|
||||
]
|
||||
})
|
||||
|
@ -1,18 +1,22 @@
|
||||
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 ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
: config.build.env
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
|
||||
var env = config.build.env
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
|
||||
rules: utils.styleLoaders({
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true
|
||||
})
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||
output: {
|
||||
@ -20,32 +24,33 @@ 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/workflow/production.html
|
||||
// http://vuejs.github.io/vue-loader/en/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: process.env.NODE_ENV === 'testing'
|
||||
? 'index.html'
|
||||
: config.build.index,
|
||||
filename: config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
@ -77,7 +82,15 @@ 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: ['.*']
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
@ -99,4 +112,9 @@ if (config.build.productionGzip) {
|
||||
)
|
||||
}
|
||||
|
||||
if (config.build.bundleAnalyzerReport) {
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
||||
|
@ -14,11 +14,17 @@ module.exports = {
|
||||
// Before setting to `true`, make sure to:
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css']
|
||||
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
|
||||
},
|
||||
dev: {
|
||||
env: require('./dev.env'),
|
||||
port: 8080,
|
||||
autoOpenBrowser: true,
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
|
@ -1,10 +1,3 @@
|
||||
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"',
|
||||
VERSION: version
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
var merge = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
@ -1,10 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OnlineJudge Admin</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<title>oj</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
5378
oj/package-lock.json
generated
Normal file
5378
oj/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
123
oj/package.json
123
oj/package.json
@ -1,89 +1,76 @@
|
||||
{
|
||||
"name": "onlinejudge_webapp",
|
||||
"name": "oj",
|
||||
"version": "1.0.0",
|
||||
"description": "A Vue.js project",
|
||||
"author": "tw1997 <t888y@qq.com>",
|
||||
"author": "zemal <rawidn@163.com>",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node build/dev-server.js",
|
||||
"start": "node build/dev-server.js",
|
||||
"build": "node build/build.js",
|
||||
"unit": "karma start test/unit/karma.conf.js --single-run",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
"lint": "eslint --ext .js,.vue src"
|
||||
},
|
||||
"dependencies": {
|
||||
"font-awesome": "^4.7.0",
|
||||
"moment": "^2.17.1",
|
||||
"promise": "^7.1.1",
|
||||
"simditor": "^2.3.6",
|
||||
"simditor-markdown": "^1.1.2",
|
||||
"vue": "^2.0.1",
|
||||
"vue-codemirror": "^3.0.6",
|
||||
"vue-i18n": "^4.7.1",
|
||||
"vue-resource": "^1.0.3",
|
||||
"vue-router": "^2.0.1"
|
||||
"iview": "^2.0.0-rc.17",
|
||||
"less": "^2.7.2",
|
||||
"less-loader": "^4.0.4",
|
||||
"moment": "^2.18.1",
|
||||
"vue": "^2.3.3",
|
||||
"vue-codemirror": "^3.0.8",
|
||||
"vue-resource": "^1.3.4",
|
||||
"vue-router": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.4.0",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-loader": "^6.0.0",
|
||||
"babel-plugin-transform-runtime": "^6.0.0",
|
||||
"babel-preset-es2015": "^6.0.0",
|
||||
"babel-preset-stage-2": "^6.0.0",
|
||||
"babel-register": "^6.0.0",
|
||||
"chai": "^3.5.0",
|
||||
"autoprefixer": "^6.7.2",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^6.2.10",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"babel-register": "^6.22.0",
|
||||
"chalk": "^1.1.3",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"cross-spawn": "^4.0.2",
|
||||
"css-loader": "^0.25.0",
|
||||
"element-ui": "^1.0.0",
|
||||
"eslint": "^3.7.1",
|
||||
"eslint-config-standard": "^6.1.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
"eslint-loader": "^1.5.0",
|
||||
"eslint-plugin-html": "^1.3.0",
|
||||
"eslint-plugin-promise": "^2.0.1",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-friendly-formatter": "^2.0.7",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-html": "^2.0.0",
|
||||
"eslint-config-standard": "^6.2.1",
|
||||
"eslint-plugin-promise": "^3.4.0",
|
||||
"eslint-plugin-standard": "^2.0.1",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.13.3",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^0.9.0",
|
||||
"function-bind": "^1.0.2",
|
||||
"html-webpack-plugin": "^2.8.1",
|
||||
"http-proxy-middleware": "^0.17.2",
|
||||
"inject-loader": "^2.0.1",
|
||||
"isparta-loader": "^2.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "^1.3.0",
|
||||
"karma-coverage": "^1.1.1",
|
||||
"karma-mocha": "^1.2.0",
|
||||
"karma-sinon-chai": "^1.2.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.26",
|
||||
"karma-webpack": "^1.7.0",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"lolex": "^1.4.0",
|
||||
"mocha": "^3.1.0",
|
||||
"nightwatch": "^0.9.8",
|
||||
"opn": "^4.0.2",
|
||||
"ora": "^0.3.0",
|
||||
"express": "^4.14.1",
|
||||
"extract-text-webpack-plugin": "^2.0.0",
|
||||
"file-loader": "^0.11.1",
|
||||
"friendly-errors-webpack-plugin": "^1.1.3",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"http-proxy-middleware": "^0.17.3",
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.4",
|
||||
"sinon": "^1.17.3",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"url-loader": "^0.5.7",
|
||||
"vue-loader": "^9.4.0",
|
||||
"vue-style-loader": "^1.0.0",
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-dev-middleware": "^1.8.3",
|
||||
"webpack-hot-middleware": "^2.12.2",
|
||||
"webpack-merge": "^0.14.1"
|
||||
"shelljs": "^0.7.6",
|
||||
"opn": "^4.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "^1.3.0",
|
||||
"ora": "^1.2.0",
|
||||
"rimraf": "^2.6.0",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^12.1.0",
|
||||
"vue-style-loader": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.3",
|
||||
"webpack": "^2.6.1",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"webpack-merge": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
||||
|
@ -11,8 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'font-awesome/css/font-awesome.min.css'
|
||||
import Navbar from './components/Navbar.vue'
|
||||
import Navbar from './NavBar.vue'
|
||||
// import api from './api.js'
|
||||
|
||||
export default {
|
||||
@ -32,31 +31,24 @@
|
||||
<style lang="less">
|
||||
|
||||
* {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: rgb(236, 242, 247) !important;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
-webkit-text-decoration-skip: objects;
|
||||
&:active, &:hover{
|
||||
outline-width:0 ;
|
||||
}
|
||||
|
||||
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;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
|
||||
overflow: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-color: #edf0f2;;
|
94
oj/src/components/NavBar.vue
Normal file
94
oj/src/components/NavBar.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div id="header">
|
||||
<Row type="flex" justify="space-around">
|
||||
<Col span="3">
|
||||
<div class="logo"><span>QduOJ</span></div>
|
||||
</Col>
|
||||
|
||||
<Col span="14">
|
||||
<Menu theme="light" mode="horizontal" @on-select="handleRoute">
|
||||
<Menu-item name="/problems">Problems</Menu-item>
|
||||
<Menu-item name="/1">Contests</Menu-item>
|
||||
<Menu-item name="/2">Status</Menu-item>
|
||||
<Menu-item name="/3">Rank</Menu-item>
|
||||
<Menu-item name="/4">About</Menu-item>
|
||||
<Menu-item name="/test">Test</Menu-item>
|
||||
</Menu>
|
||||
</Col>
|
||||
|
||||
<Col span="4">
|
||||
<template v-if="!isAuthed">
|
||||
<div class="btn-menu">
|
||||
<Button type="ghost" shape="circle" @click="handleRoute('/login')">Login</Button>
|
||||
<Button type="ghost" shape="circle" @click="handleRoute('/register')" style="margin-left: 5px;">Register
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Dropdown class="right" @on-click="handleRoute">
|
||||
<Button class="btn-menu" type="text">zemal
|
||||
<Icon type="arrow-down-b"></Icon>
|
||||
</Button>
|
||||
<Dropdown-menu slot="list">
|
||||
<Dropdown-item>Home</Dropdown-item>
|
||||
<Dropdown-item>Submissions</Dropdown-item>
|
||||
<Dropdown-item name="/settings">Settings</Dropdown-item>
|
||||
<Dropdown-item divided name="/logout">Logout</Dropdown-item>
|
||||
</Dropdown-menu>
|
||||
</Dropdown>
|
||||
</template>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../utils/eventBus'
|
||||
// import auth from '../utils/authHelper'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isAuthed: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRoute(route) {
|
||||
this.$router.push(route)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
bus.$on('loginSuccess', (res) => {
|
||||
this.isAuthed = true
|
||||
})
|
||||
bus.$on('logout', () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #dddee1;
|
||||
.logo {
|
||||
margin-left: 20px;
|
||||
font-size: 20px;
|
||||
float: left;
|
||||
line-height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.ivu-menu-horizontal.ivu-menu-light:after {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.btn-menu {
|
||||
font-size: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div class="nav">
|
||||
<el-row :gutter="10" type="flex" justify="space-around">
|
||||
<el-col :xs="1" :sm="1" :lg="1"> </el-col>
|
||||
<el-col :xs="2" :sm="2" :lg="2">
|
||||
<div id="brand">QDUOJ</div>
|
||||
</el-col>
|
||||
<el-col :xs="16" :sm="16" :lg="16">
|
||||
<el-menu theme="dark" mode="horizontal" router>
|
||||
<el-menu-item index="/problems">Problems</el-menu-item>
|
||||
<el-menu-item index="3">Contests</el-menu-item>
|
||||
<el-menu-item index="2">Status</el-menu-item>
|
||||
<el-menu-item index="4">Rank</el-menu-item>
|
||||
<el-menu-item index="5">About</el-menu-item>
|
||||
</el-menu>
|
||||
</el-col>
|
||||
<el-col :xs="3" :sm="3" :lg="3">
|
||||
<template v-if="isLogin">
|
||||
<el-menu theme="dark" mode="horizontal" router>
|
||||
<el-submenu index="2">
|
||||
<template slot="title">{{ info.user.username }}
|
||||
</template>
|
||||
<el-menu-item index="2-1" :route="{path:'/user/'+info.user.username}">Home</el-menu-item>
|
||||
<el-menu-item index="2-2">Submissions</el-menu-item>
|
||||
<el-menu-item index="/setting">Settings</el-menu-item>
|
||||
<el-menu-item index="/logout">Logout</el-menu-item>
|
||||
</el-submenu>
|
||||
</el-menu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="">
|
||||
<el-menu theme="dark" mode="horizontal" router>
|
||||
<el-menu-item class="login-btn" index="/login">Login</el-menu-item>
|
||||
<el-menu-item class="login-separator" index="/">/</el-menu-item>
|
||||
<el-menu-item class="login-btn" index="/register">Register</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import api from '../api.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isLogin: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getMyInfo()
|
||||
},
|
||||
watch: {
|
||||
// 如果路由有变化,会再次执行该方法
|
||||
'$route': 'getMyInfo'
|
||||
},
|
||||
methods: {
|
||||
getMyInfo() {
|
||||
api.getMyInfo().then(res => {
|
||||
this.info = res.data.data
|
||||
this.isLogin = true
|
||||
}, res => {
|
||||
this.isLogin = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.nav {
|
||||
background-color: #324157;
|
||||
}
|
||||
|
||||
.el-menu-item{
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#brand {
|
||||
font-size: 23px;
|
||||
display: inline-block;
|
||||
line-height: 60px;
|
||||
color: #E5E9F2;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.login-separator {
|
||||
padding: 0 10px !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.login-separator.is-active {
|
||||
color: #bfcbd9 !important;
|
||||
}
|
||||
</style>
|
140
oj/src/main.js
140
oj/src/main.js
@ -1,135 +1,39 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App'
|
||||
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'
|
||||
import App from './components/App.vue'
|
||||
import iView from 'iview'
|
||||
import router from './routers/index'
|
||||
import 'iview/dist/styles/iview.css'
|
||||
import 'font-awesome/css/font-awesome.min.css'
|
||||
|
||||
import * as filters from './filters.js'
|
||||
import * as filters from './utils/filters.js'
|
||||
|
||||
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'
|
||||
// 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(iView)
|
||||
// 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 { Problem, Contest, ContestList,
|
||||
ContestAnnouncement, ProblemList, Login, Register, Logout, Profile,
|
||||
ProfileSetting, AvatarSetting } from './views'
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
scrollBehavior: () => ({ y: 0 }),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: Register
|
||||
},
|
||||
{
|
||||
path: '/logout',
|
||||
name: 'logout',
|
||||
component: Logout
|
||||
},
|
||||
{
|
||||
path: '/user/:userId',
|
||||
name: 'user-profile',
|
||||
component: Profile
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
name: 'profile-setting',
|
||||
component: ProfileSetting
|
||||
},
|
||||
{
|
||||
path: '/setting/avatar',
|
||||
name: 'avatar-setting',
|
||||
component: AvatarSetting
|
||||
},
|
||||
// 隔开
|
||||
{
|
||||
path: '/problems',
|
||||
name: 'problem-list',
|
||||
component: ProblemList
|
||||
},
|
||||
{
|
||||
path: '/problem/:id',
|
||||
name: 'problem-details',
|
||||
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: '/problems'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
Vue.prototype.$error = (msg) => {
|
||||
Vue.prototype.$message({ 'message': msg, 'type': 'error' })
|
||||
}
|
||||
|
||||
Vue.prototype.$alert = (msg) => {
|
||||
Vue.prototype.$message({ 'message': msg, 'type': 'info' })
|
||||
}
|
||||
// Vue.component(IconBtn.name, IconBtn)
|
||||
// Vue.component(Panel.name, Panel)
|
||||
// Vue.component(Save.name, Save)
|
||||
// Vue.component(Cancel.name, Cancel)
|
||||
|
||||
// 注册全局消息提示
|
||||
Vue.prototype.$error = Vue.prototype.$Message.error
|
||||
Vue.prototype.$info = Vue.prototype.$Message.info
|
||||
Vue.prototype.$success = (msg) => {
|
||||
if (!msg) {
|
||||
Vue.prototype.$message({ 'message': 'Succeeded', 'type': 'success' })
|
||||
Vue.prototype.$Message.success('Success')
|
||||
} else {
|
||||
Vue.prototype.$message({ 'message': msg, 'type': 'success' })
|
||||
Vue.prototype.$Message.success(msg)
|
||||
}
|
||||
}
|
||||
Vue.prototype.$Message.success('启动成功')
|
||||
new Vue(Vue.util.extend({router}, App)).$mount('#app')
|
||||
|
||||
new Vue(Vue.util.extend({ router }, App)).$mount('#app')
|
||||
|
32
oj/src/routers/index.js
Normal file
32
oj/src/routers/index.js
Normal file
@ -0,0 +1,32 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
import Test from '../views/test'
|
||||
import Login from './routes/login'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
},
|
||||
routes: [
|
||||
Login,
|
||||
{
|
||||
path: '/test',
|
||||
name: 'Test',
|
||||
component: Test
|
||||
},
|
||||
{
|
||||
path: '*', redirect: '/login'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
|
14
oj/src/routers/routes/login.js
Normal file
14
oj/src/routers/routes/login.js
Normal file
@ -0,0 +1,14 @@
|
||||
// import auth from '@/utils/authHelper'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
component: () => import('@/views/user/Login.vue')
|
||||
// beforeEnter(to, from, next) {
|
||||
// if (auth.isAuthicated()) {
|
||||
// next('/test')
|
||||
// } else {
|
||||
// next()
|
||||
// }
|
||||
// }
|
||||
}
|
30
oj/src/utils/authHelper.js
Normal file
30
oj/src/utils/authHelper.js
Normal file
@ -0,0 +1,30 @@
|
||||
const storage = window.localStorage
|
||||
|
||||
export default {
|
||||
name: 'authHelper',
|
||||
/**
|
||||
* 将用户信息存入localstorage
|
||||
* @param userInfo
|
||||
*/
|
||||
set(userInfo) {
|
||||
storage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @returns {object}
|
||||
*/
|
||||
get() {
|
||||
return JSON.parse(storage.getItem('userInfo')) || null
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除用户信息
|
||||
*/
|
||||
remove() {
|
||||
storage.removeItem('userInfo')
|
||||
},
|
||||
isAuthicated() {
|
||||
return !!storage.getItem('userInfo')
|
||||
}
|
||||
}
|
6
oj/src/utils/eventBus.js
Normal file
6
oj/src/utils/eventBus.js
Normal file
@ -0,0 +1,6 @@
|
||||
// 这个文件用来做同级组件间的数据传递,见Login.vue
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
export default new Vue()
|
||||
|
@ -1,144 +0,0 @@
|
||||
<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>
|
@ -1,106 +0,0 @@
|
||||
<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>
|
@ -1,116 +0,0 @@
|
||||
<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>
|
@ -1,17 +1,20 @@
|
||||
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'
|
||||
import Login from './user/Login.vue'
|
||||
import Register from './user/Register.vue'
|
||||
import Logout from './user/Logout.vue'
|
||||
import Profile from './user/Profile.vue'
|
||||
import ProfileSetting from './setting/ProfileSetting.vue'
|
||||
import AvatarSetting from './setting/AvatarSetting.vue'
|
||||
/* 组件导出分为两类, 一类常用的直接导出,另一类诸如Login, Logout等用懒加载,懒加载不在此处导出
|
||||
在对应的route内加载
|
||||
见https://router.vuejs.org/en/advanced/lazy-loading.html
|
||||
*/
|
||||
|
||||
export {
|
||||
Problem, ProblemList, Contest,
|
||||
ContestList, ContestAnnouncement, Login, Register, Logout, Profile,
|
||||
ProfileSetting, AvatarSetting
|
||||
}
|
||||
// import Problem from './problem/Problem.vue'
|
||||
// import ProblemList from './problem/ProblemList.vue'
|
||||
// import Register from './user/Register.vue'
|
||||
// import Logout from './user/Logout.vue'
|
||||
// import Profile from './user/Profile.vue'
|
||||
// import ProfileSetting from './setting/ProfileSetting.vue'
|
||||
// import AvatarSetting from './setting/AvatarSetting.vue'
|
||||
//
|
||||
// export {
|
||||
// Problem, ProblemList,
|
||||
// Login, Register, Logout, Profile,
|
||||
// ProfileSetting, AvatarSetting
|
||||
// }
|
||||
|
||||
// export {}
|
||||
|
@ -78,7 +78,7 @@
|
||||
import CodeMirror from '../../components/CodeMirror'
|
||||
import Card from '../../components/Card'
|
||||
import api from '../../api'
|
||||
import {STATUS} from '../../utils'
|
||||
import {STATUS} from '../../utils/utils'
|
||||
import Panel from '../../components/Panel'
|
||||
|
||||
export default {
|
||||
|
20
oj/src/views/test.vue
Normal file
20
oj/src/views/test.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
Hello world
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
export default {
|
||||
mounted() {
|
||||
api.getProblem('1').then((res) => {
|
||||
console.log(res.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
@ -1,39 +1,35 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<el-card class="box-card" id="login-card">
|
||||
|
||||
<div slot="header" class="clearfix" id="login-title">
|
||||
<span style="line-height: 36px;">用户登录</span>
|
||||
<div class="center">
|
||||
<Card id="login-card">
|
||||
<div slot="title" class="clearfix" id="login-title">
|
||||
<span style="line-height: 36px;">OnlineJudge</span>
|
||||
</div>
|
||||
<Form :model="loginForm" :label-width="80">
|
||||
<Form-item label="Username">
|
||||
<Input v-model="loginForm.username"/>
|
||||
</Form-item>
|
||||
<Form-item label="Password">
|
||||
<Input v-model="loginForm.password"/>
|
||||
</Form-item>
|
||||
<Form-item>
|
||||
<Button type="primary" id="btn-login" @click="onSubmit">Login</Button>
|
||||
</Form-item>
|
||||
</Form>
|
||||
|
||||
<el-form :label-position="labelPosition" label-width="80px" :model="formLabelAlign">
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="formLabelAlign.username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="formLabelAlign.password" type="password"></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="二次验证">
|
||||
<el-input v-model="formLabelAlign.totp"></el-input>
|
||||
</el-form-item> -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSubmit">立即登录</el-button>
|
||||
<el-button>用户注册</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '../../api.js'
|
||||
import api from '../../api.js'
|
||||
import bus from '../../utils/eventBus'
|
||||
// import auth from '@/utils/authHelper'
|
||||
|
||||
export default {
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
labelPosition: 'top',
|
||||
formLabelAlign: {
|
||||
loginForm: {
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
@ -41,27 +37,33 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
console.log('submit!')
|
||||
api.login(this.formLabelAlign.username, this.formLabelAlign.password).then(function(res) {
|
||||
console.log(res)
|
||||
api.devLogin(this.loginForm.username, this.loginForm.password).then((res) => {
|
||||
bus.$emit('loginSuccess', res)
|
||||
// auth.set(res.data)
|
||||
}, (res) => {
|
||||
})
|
||||
// 不会自动刷新 header
|
||||
this.$router.replace({ path: '/' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.center{
|
||||
.center {
|
||||
margin-top: 50px;
|
||||
}
|
||||
#login-card{
|
||||
}
|
||||
|
||||
#login-card {
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
}
|
||||
#login-title{
|
||||
}
|
||||
|
||||
#login-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#btn-login {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user