├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.dll.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js └── prod.env.js ├── deploy ├── Dockerfile ├── nginx.conf ├── run.sh └── sentry_release.sh ├── package.json ├── src ├── assets │ ├── Cup.png │ └── logo.svg ├── i18n │ ├── admin │ │ ├── en-US.js │ │ ├── zh-CN.js │ │ └── zh-TW.js │ ├── index.js │ └── oj │ │ ├── en-US.js │ │ ├── zh-CN.js │ │ └── zh-TW.js ├── pages │ ├── admin │ │ ├── App.vue │ │ ├── api.js │ │ ├── components │ │ │ ├── Accordion.vue │ │ │ ├── CodeMirror.vue │ │ │ ├── KatexEditor.vue │ │ │ ├── Panel.vue │ │ │ ├── ScreenFull.vue │ │ │ ├── SideMenu.vue │ │ │ ├── Simditor.vue │ │ │ ├── TopNav.vue │ │ │ ├── btn │ │ │ │ ├── Cancel.vue │ │ │ │ ├── IconBtn.vue │ │ │ │ └── Save.vue │ │ │ ├── infoCard.vue │ │ │ └── simditor-file-upload.js │ │ ├── index.html │ │ ├── index.js │ │ ├── router.js │ │ ├── style.less │ │ └── views │ │ │ ├── Home.vue │ │ │ ├── contest │ │ │ ├── Contest.vue │ │ │ └── ContestList.vue │ │ │ ├── general │ │ │ ├── Announcement.vue │ │ │ ├── Conf.vue │ │ │ ├── Dashboard.vue │ │ │ ├── JudgeServer.vue │ │ │ ├── Login.vue │ │ │ ├── PruneTestCase.vue │ │ │ └── User.vue │ │ │ ├── index.js │ │ │ └── problem │ │ │ ├── AddPublicProblem.vue │ │ │ ├── ImportAndExport.vue │ │ │ ├── Problem.vue │ │ │ └── ProblemList.vue │ └── oj │ │ ├── App.vue │ │ ├── api.js │ │ ├── components │ │ ├── CodeMirror.vue │ │ ├── Highlight.vue │ │ ├── NavBar.vue │ │ ├── Pagination.vue │ │ ├── Panel.vue │ │ ├── mixins │ │ │ ├── emitter.js │ │ │ ├── form.js │ │ │ ├── index.js │ │ │ └── problem.js │ │ └── verticalMenu │ │ │ ├── verticalMenu-item.vue │ │ │ └── verticalMenu.vue │ │ ├── index.html │ │ ├── index.js │ │ ├── router │ │ ├── index.js │ │ └── routes.js │ │ └── views │ │ ├── contest │ │ ├── ContestDetail.vue │ │ ├── ContestList.vue │ │ ├── children │ │ │ ├── ACMContestRank.vue │ │ │ ├── ACMHelper.vue │ │ │ ├── ContestProblemList.vue │ │ │ ├── ContestRank.vue │ │ │ ├── OIContestRank.vue │ │ │ └── contestRankMixin.js │ │ └── index.js │ │ ├── general │ │ ├── 404.vue │ │ ├── Announcements.vue │ │ └── Home.vue │ │ ├── help │ │ ├── About.vue │ │ └── FAQ.vue │ │ ├── index.js │ │ ├── problem │ │ ├── Problem.vue │ │ ├── ProblemList.vue │ │ └── chartData.js │ │ ├── rank │ │ ├── ACMRank.vue │ │ └── OIRank.vue │ │ ├── setting │ │ ├── Settings.vue │ │ ├── children │ │ │ ├── AccountSetting.vue │ │ │ ├── ProfileSetting.vue │ │ │ └── SecuritySetting.vue │ │ └── index.js │ │ ├── submission │ │ ├── SubmissionDetails.vue │ │ └── SubmissionList.vue │ │ └── user │ │ ├── ApplyResetPassword.vue │ │ ├── Login.vue │ │ ├── Logout.vue │ │ ├── Register.vue │ │ ├── ResetPassword.vue │ │ └── UserHome.vue ├── plugins │ ├── highlight.js │ └── katex.js ├── store │ ├── index.js │ ├── modules │ │ ├── contest.js │ │ └── user.js │ └── types.js ├── styles │ ├── common.less │ ├── index.less │ ├── iview-custom.less │ └── markdown.less └── utils │ ├── constants.js │ ├── filters.js │ ├── sentry.js │ ├── storage.js │ ├── time.js │ └── utils.js ├── static └── css │ └── loader.css └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 9"] 7 | }, 8 | "useBuiltIns": true 9 | }], 10 | "stage-2" 11 | ], 12 | "plugins": [ 13 | "transform-runtime", 14 | "syntax-dynamic-import" 15 | ], 16 | "env": { 17 | "test": { 18 | "presets": [ 19 | "env", 20 | "stage-2" 21 | ], 22 | "plugins": [ 23 | "istanbul" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | extends: 'standard', 9 | // required to lint *.vue files 10 | plugins: [ 11 | 'html' 12 | ], 13 | // add your custom rules here 14 | 'rules': { 15 | // allow paren-less arrow functions 16 | 'arrow-parens': 0, 17 | // allow async-await 18 | 'generator-star-spacing': 0, 19 | // allow debugger during development 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 21 | "no-irregular-whitespace": ["error", { 22 | "skipComments": true, 23 | "skipTemplates": true 24 | }], 25 | "no-unused-vars": ["warn"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | dist/ 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # editor 63 | .vscode 64 | .idea 65 | 66 | # test_code 67 | test.vue 68 | 69 | # build 70 | vendor-manifest.json 71 | vendor.dll*.js 72 | 73 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8.12.0 4 | sudo: required 5 | services: 6 | - docker 7 | env: 8 | global: 9 | - CXX=g++-4.8 10 | matrix: 11 | - USE_SENTRY=1 12 | # - USE_SENTRY=0 13 | addons: 14 | apt: 15 | sources: 16 | - ubuntu-toolchain-r-test 17 | packages: 18 | - g++-4.8 19 | before_install: 20 | - docker pull getsentry/sentry-cli 21 | 22 | script: 23 | - npm install 24 | - npm run build:dll 25 | - npm run build 26 | 27 | before_deploy: 28 | - bash deploy/sentry_release.sh 29 | - find dist/ -type f -name "*.map" -delete 30 | - zip -r dist.zip dist 31 | 32 | deploy: 33 | provider: releases 34 | skip_cleanup: true 35 | api_key: 36 | secure: I+BjkiPd+7XnMo1qWFhyz2xqElBLlGuCx1ZamvEa61zk4HPAnfWDPvpxXWihLdE4WDIkrOTRQ6Nh6GlVz+2uFugtdAlrxEWjU3bhadf5hAPNL2faNd52CN2qRNOuaWZmIkY4KUDmKoxvcFVQCKqMCxpghlNT7IJLwaC5wjogMeQKERXSa4rXl/ZGHdZzpkPo8SIEIy7hwQcHDl0ckm3t8wlXe6/xCRR2YoI3enZE15oJ15PChEcYbI6kUHNg3iIAddAAykqU0PnBtGSaPkl2JU90f4Rs62wW626wXyBs39ZU2rSbooyKF7OgFtS7KeTbYxyINBta8JH45plE/HVB3U3wy/8dBneZYr6ySGtSZSV10ortW59Al6Pifyo1na6tgkXSrckUOh1HFSiOsN6k46RXo6T1L1w1P8cUCJ2WYJksHJqBXnQmKbol9x3Gz6fQHR6yA5ToczKmg2ow549Q3g3oRb6RjRzXNkYu9sAZ2mhQ1fIjm7GkYPQvHzze+qwBxeX/ysFwIUNjUnK6u6EZunShz6fF9NEsDxzfUXPDOfB28MmxGkIi6TuT21F8tKfCG0C0CqLWBBve2agU9gutvIA1aY9i5K0YnlXPah1NYYzDPlk1RepWzJrq9VzGz3HxwY++cm6vR/7o9V+3WVpMIWHjnYwDTpK7pfm3VYbiLlg= 37 | file: dist.zip 38 | on: 39 | repo: QingdaoU/OnlineJudgeFE 40 | all_branches: true 41 | tags: true 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present OnineJudge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | The MIT License (MIT) 24 | 25 | Copyright (c) 2013-present, Yuxi (Evan) You 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in 35 | all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 43 | THE SOFTWARE. 44 | 45 | The MIT License (MIT) 46 | 47 | 48 | Copyright (c) 2016-present iView 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy 51 | of this software and associated documentation files (the "Software"), to deal 52 | in the Software without restriction, including without limitation the rights 53 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 54 | copies of the Software, and to permit persons to whom the Software is 55 | furnished to do so, subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in all 58 | copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 62 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 63 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 64 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 65 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 66 | SOFTWARE. 67 | 68 | The MIT License (MIT) 69 | 70 | Copyright (c) 2016 ElemeFE 71 | 72 | Permission is hereby granted, free of charge, to any person obtaining a copy 73 | of this software and associated documentation files (the "Software"), to deal 74 | in the Software without restriction, including without limitation the rights 75 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 76 | copies of the Software, and to permit persons to whom the Software is 77 | furnished to do so, subject to the following conditions: 78 | 79 | The above copyright notice and this permission notice shall be included in all 80 | copies or substantial portions of the Software. 81 | 82 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 83 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 84 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 85 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 86 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 87 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 88 | SOFTWARE. 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OnlineJudge Front End 2 | [![vue](https://img.shields.io/badge/vue-2.5.13-blue.svg?style=flat-square)](https://github.com/vuejs/vue) 3 | [![vuex](https://img.shields.io/badge/vuex-3.0.1-blue.svg?style=flat-square)](https://vuex.vuejs.org/) 4 | [![echarts](https://img.shields.io/badge/echarts-3.8.3-blue.svg?style=flat-square)](https://github.com/ecomfe/echarts) 5 | [![iview](https://img.shields.io/badge/iview-2.8.0-blue.svg?style=flat-square)](https://github.com/iview/iview) 6 | [![element-ui](https://img.shields.io/badge/element-2.0.9-blue.svg?style=flat-square)](https://github.com/ElemeFE/element) 7 | [![Build Status](https://travis-ci.org/QingdaoU/OnlineJudgeFE.svg?branch=master)](https://travis-ci.org/QingdaoU/OnlineJudgeFE) 8 | 9 | >### A multiple pages app built for OnlineJudge. [Demo](https://qduoj.com) 10 | 11 | ## Features 12 | 13 | + Webpack3 multiple pages with bundle size optimization 14 | + Easy use simditor & Nice codemirror editor 15 | + Amazing charting and visualization(echarts) 16 | + User-friendly operation 17 | + Quite beautiful:) 18 | 19 | ## Get Started 20 | 21 | Install nodejs **v8.12.0** first. 22 | 23 | ### Linux 24 | 25 | ```bash 26 | npm install 27 | # we use webpack DllReference to decrease the build time, 28 | # this command only needs execute once unless you upgrade the package in build/webpack.dll.conf.js 29 | export NODE_ENV=development 30 | npm run build:dll 31 | 32 | # the dev-server will set proxy table to your backend 33 | export TARGET=http://Your-backend 34 | 35 | # serve with hot reload at localhost:8080 36 | npm run dev 37 | ``` 38 | ### Windows 39 | 40 | ```bash 41 | npm install 42 | # we use webpack DllReference to decrease the build time, 43 | # this command only needs execute once unless you upgrade the package in build/webpack.dll.conf.js 44 | set NODE_ENV=development 45 | npm run build:dll 46 | 47 | # the dev-server will set proxy table to your backend 48 | set TARGET=http://Your-backend 49 | 50 | # serve with hot reload at localhost:8080 51 | npm run dev 52 | ``` 53 | 54 | ## Screenshots 55 | 56 | [Check here.](https://github.com/QingdaoU/OnlineJudge) 57 | 58 | ## Browser Support 59 | 60 | Modern browsers and Internet Explorer 10+. 61 | 62 | ## LICENSE 63 | 64 | [MIT](http://opensource.org/licenses/MIT) 65 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, function (err, stats) { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Congratulations, the project built complete without error\n')) 36 | console.log(chalk.yellow( 37 | ' You can now check the onlinejudge in http://YouIP/' 38 | )) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | function exec (cmd) { 7 | return require('child_process').execSync(cmd).toString().trim() 8 | } 9 | 10 | const versionRequirements = [ 11 | { 12 | name: 'node', 13 | currentVersion: semver.clean(process.version), 14 | versionRequirement: packageConfig.engines.node 15 | } 16 | ] 17 | 18 | if (shell.which('npm')) { 19 | versionRequirements.push({ 20 | name: 'npm', 21 | currentVersion: exec('npm --version'), 22 | versionRequirement: packageConfig.engines.npm 23 | }) 24 | } 25 | 26 | module.exports = function () { 27 | const warnings = [] 28 | for (let i = 0; i < versionRequirements.length; i++) { 29 | const mod = versionRequirements[i] 30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 31 | warnings.push(mod.name + ': ' + 32 | chalk.red(mod.currentVersion) + ' should be ' + 33 | chalk.green(mod.versionRequirement) 34 | ) 35 | } 36 | } 37 | 38 | if (warnings.length) { 39 | console.log('') 40 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 41 | console.log() 42 | for (let i = 0; i < warnings.length; i++) { 43 | const warning = warnings[i] 44 | console.log(' ' + warning) 45 | } 46 | console.log() 47 | process.exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict' 3 | require('eventsource-polyfill') 4 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 5 | 6 | hotClient.subscribe(function (event) { 7 | if (event.action === 'reload') { 8 | window.location.reload() 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | const config = require('../config') 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 7 | } 8 | 9 | const opn = require('opn') 10 | const path = require('path') 11 | const express = require('express') 12 | const webpack = require('webpack') 13 | const proxyMiddleware = require('http-proxy-middleware') 14 | const webpackConfig = require('./webpack.dev.conf') 15 | 16 | // default port where dev server listens for incoming traffic 17 | const port = process.env.PORT || config.dev.port 18 | // automatically open browser, if not set will be false 19 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 20 | // Define HTTP proxies to your custom API backend 21 | // https://github.com/chimurai/http-proxy-middleware 22 | const proxyTable = config.dev.proxyTable 23 | 24 | const app = express() 25 | const compiler = webpack(webpackConfig) 26 | 27 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 28 | publicPath: webpackConfig.output.publicPath, 29 | quiet: true 30 | }) 31 | 32 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 33 | log: false, 34 | heartbeat: 2000 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | // currently disabled until this is resolved: 38 | // https://github.com/jantimon/html-webpack-plugin/issues/680 39 | // compiler.plugin('compilation', function (compilation) { 40 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 41 | // hotMiddleware.publish({ action: 'reload' }) 42 | // cb() 43 | // }) 44 | // }) 45 | 46 | // enable hot-reload and state-preserving 47 | // compilation error display 48 | app.use(hotMiddleware) 49 | 50 | // proxy api requests 51 | Object.keys(proxyTable).forEach(function (context) { 52 | let options = proxyTable[context] 53 | if (typeof options === 'string') { 54 | options = { target: options } 55 | } 56 | app.use(proxyMiddleware(options.filter || context, options)) 57 | }) 58 | 59 | // handle fallback for HTML5 history API 60 | const rewrites = { 61 | rewrites: [{ 62 | from: '/admin/', // 正则或者字符串 63 | to: '/admin/index.html', // 字符串或者函数 64 | }] 65 | } 66 | const historyMiddleware = require('connect-history-api-fallback')(rewrites); 67 | app.use(historyMiddleware) 68 | 69 | // serve webpack bundle output 70 | app.use(devMiddleware) 71 | 72 | // serve pure static assets 73 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 74 | app.use(staticPath, express.static('./static')) 75 | 76 | const uri = 'http://localhost:' + port 77 | 78 | var _resolve 79 | var _reject 80 | var readyPromise = new Promise((resolve, reject) => { 81 | _resolve = resolve 82 | _reject = reject 83 | }) 84 | 85 | var server 86 | var portfinder = require('portfinder') 87 | portfinder.basePort = port 88 | 89 | console.log('> Starting dev server...') 90 | devMiddleware.waitUntilValid(() => { 91 | portfinder.getPort((err, port) => { 92 | if (err) { 93 | _reject(err) 94 | } 95 | process.env.PORT = port 96 | var uri = 'http://localhost:' + port 97 | console.log('> Listening at ' + uri + '\n') 98 | // when env is testing, don't need open it 99 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 100 | opn(uri) 101 | } 102 | server = app.listen(port) 103 | _resolve() 104 | }) 105 | }) 106 | 107 | module.exports = { 108 | ready: readyPromise, 109 | close: () => { 110 | server.close() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | exports.assetsPath = function (_path) { 7 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 8 | ? config.build.assetsSubDirectory 9 | : config.dev.assetsSubDirectory 10 | return path.posix.join(assetsSubDirectory, _path) 11 | } 12 | 13 | exports.cssLoaders = function (options) { 14 | options = options || {} 15 | 16 | const cssLoader = { 17 | loader: 'css-loader', 18 | options: { 19 | minimize: process.env.NODE_ENV === 'production', 20 | sourceMap: options.sourceMap 21 | } 22 | } 23 | 24 | // generate loader string to be used with extract text plugin 25 | function generateLoaders (loader, loaderOptions) { 26 | const loaders = [cssLoader] 27 | if (loader) { 28 | loaders.push({ 29 | loader: loader + '-loader', 30 | options: Object.assign({}, loaderOptions, { 31 | sourceMap: options.sourceMap 32 | }) 33 | }) 34 | } 35 | 36 | // Extract CSS when that option is specified 37 | // (which is the case during production build) 38 | if (options.extract) { 39 | return ExtractTextPlugin.extract({ 40 | use: loaders, 41 | fallback: 'vue-style-loader' 42 | }) 43 | } else { 44 | return ['vue-style-loader'].concat(loaders) 45 | } 46 | } 47 | 48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 49 | return { 50 | css: generateLoaders(), 51 | postcss: generateLoaders(), 52 | less: generateLoaders('less'), 53 | sass: generateLoaders('sass', {indentedSyntax: true}), 54 | scss: generateLoaders('sass'), 55 | stylus: generateLoaders('stylus'), 56 | styl: generateLoaders('stylus') 57 | } 58 | } 59 | 60 | // Generate loaders for standalone style files (outside of .vue) 61 | exports.styleLoaders = function (options) { 62 | const output = [] 63 | const loaders = exports.cssLoaders(options) 64 | for (const extension in loaders) { 65 | const loader = loaders[extension] 66 | output.push({ 67 | test: new RegExp('\\.' + extension + '$'), 68 | use: loader 69 | }) 70 | } 71 | return output 72 | } 73 | 74 | exports.getNodeEnv = function () { 75 | const NODE_ENV = process.env.NODE_ENV 76 | return NODE_ENV ? NODE_ENV: 'production' 77 | } 78 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | loaders: utils.cssLoaders({ 8 | sourceMap: isProduction 9 | ? config.build.productionSourceMap 10 | : config.dev.cssSourceMap, 11 | extract: isProduction 12 | }), 13 | transformToRequire: { 14 | video: 'src', 15 | source: 'src', 16 | img: 'src', 17 | image: 'xlink:href' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const glob = require('glob') 4 | const webpack = require('webpack') 5 | const utils = require('./utils') 6 | const config = require('../config') 7 | const vueLoaderConfig = require('./vue-loader.conf') 8 | const HtmlWebpackIncludeAssetsPlugin = require('html-webpack-include-assets-plugin') 9 | 10 | function resolve (dir) { 11 | return path.join(__dirname, '..', dir) 12 | } 13 | 14 | function getEntries () { 15 | const base = { 16 | 'oj': ['./src/pages/oj/index.js'], 17 | 'admin': ['./src/pages/admin/index.js'] 18 | } 19 | if (process.env.USE_SENTRY === '1') { 20 | Object.keys(base).forEach(entry => { 21 | base[entry].push('./src/utils/sentry.js') 22 | }) 23 | } 24 | return base 25 | } 26 | 27 | // get all entries 28 | const entries = getEntries() 29 | console.log("All entries: ") 30 | Object.keys(entries).forEach(entry => { 31 | console.log(entry) 32 | entries[entry].forEach(ele => { 33 | console.log("- %s", ele) 34 | }) 35 | console.log() 36 | }) 37 | 38 | // prepare vendor asserts 39 | const globOptions = {cwd: resolve('static/js')}; 40 | let vendorAssets = glob.sync('vendor.dll.*.js', globOptions); 41 | vendorAssets = vendorAssets.map(file => 'static/js/' + file) 42 | 43 | 44 | module.exports = { 45 | entry: entries, 46 | output: { 47 | path: config.build.assetsRoot, 48 | filename: '[name].js', 49 | publicPath: process.env.NODE_ENV === 'production' 50 | ? config.build.assetsPublicPath 51 | : config.dev.assetsPublicPath 52 | }, 53 | resolve: { 54 | modules: ['node_modules'], 55 | extensions: ['.js', '.vue', '.json'], 56 | alias: { 57 | 'vue$': 'vue/dist/vue.esm.js', 58 | '@': resolve('src'), 59 | '@oj': resolve('src/pages/oj'), 60 | '@admin': resolve('src/pages/admin'), 61 | '~': resolve('src/components') 62 | } 63 | }, 64 | module: { 65 | rules: [ 66 | // { 67 | // test: /\.(js|vue)$/, 68 | // loader: 'eslint-loader', 69 | // enforce: 'pre', 70 | // include: [resolve('src')], 71 | // options: { 72 | // formatter: require('eslint-friendly-formatter') 73 | // } 74 | // }, 75 | { 76 | test: /\.vue$/, 77 | loader: 'vue-loader', 78 | options: vueLoaderConfig 79 | }, 80 | { 81 | test: /\.js$/, 82 | loader: 'babel-loader?cacheDirectory=true', 83 | exclude: /node_modules/, 84 | include: [resolve('src'), resolve('test')] 85 | }, 86 | { 87 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 88 | loader: 'url-loader', 89 | options: { 90 | limit: 10000, 91 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 92 | } 93 | }, 94 | { 95 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 96 | loader: 'url-loader', 97 | options: { 98 | limit: 10000, 99 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 100 | } 101 | }, 102 | { 103 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 104 | loader: 'url-loader', 105 | options: { 106 | limit: 10000, 107 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 108 | } 109 | } 110 | ] 111 | }, 112 | plugins: [ 113 | new webpack.DllReferencePlugin({ 114 | context: __dirname, 115 | manifest: require('./vendor-manifest.json') 116 | }), 117 | new HtmlWebpackIncludeAssetsPlugin({ 118 | assets: [vendorAssets[0]], 119 | files: ['index.html', 'admin/index.html'], 120 | append: false 121 | }), 122 | ] 123 | } 124 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 9 | 10 | // add hot-reload related code to entry chunks 11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 12 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 13 | }) 14 | 15 | module.exports = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 18 | }, 19 | // cheap-module-eval-source-map is faster for development 20 | devtool: '#cheap-module-eval-source-map', 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': config.dev.env 24 | }), 25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoEmitOnErrorsPlugin(), 28 | 29 | // https://github.com/ampedandwired/html-webpack-plugin 30 | new HtmlWebpackPlugin({ 31 | filename: config.build.ojIndex, 32 | template: config.build.ojTemplate, 33 | chunks: ['oj'], 34 | inject: true 35 | }), 36 | new HtmlWebpackPlugin({ 37 | filename: config.build.adminIndex, 38 | template: config.build.adminTemplate, 39 | chunks: ['admin'], 40 | inject: true 41 | }), 42 | new FriendlyErrorsPlugin() 43 | ] 44 | }) 45 | -------------------------------------------------------------------------------- /build/webpack.dll.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 4 | const config = require('../config') 5 | const utils = require('./utils') 6 | const glob = require('glob') 7 | const fs = require('fs') 8 | 9 | function resolve (dir) { 10 | return path.join(__dirname, '..', dir) 11 | } 12 | 13 | const NODE_ENV = utils.getNodeEnv() 14 | 15 | const vendors = [ 16 | 'vue/dist/vue.esm.js', 17 | 'vue-router', 18 | 'vuex', 19 | 'axios', 20 | 'moment', 21 | 'raven-js', 22 | 'browser-detect' 23 | ]; 24 | 25 | // clear old dll 26 | const globOptions = {cwd: resolve('static/js'), absolute: true}; 27 | let oldDlls = glob.sync('vendor.dll.*.js', globOptions); 28 | console.log("cleaning old dll..") 29 | oldDlls.forEach(f => { 30 | fs.unlink(f, _ => {}) 31 | }) 32 | console.log("building ..") 33 | 34 | module.exports = { 35 | entry: { 36 | "vendor": vendors, 37 | }, 38 | output: { 39 | path: path.join(__dirname, '../static/js'), 40 | filename: '[name].dll.[hash:7].js', 41 | library: '[name]_[hash]_dll', 42 | }, 43 | plugins: [ 44 | new webpack.DefinePlugin({ 45 | 'process.env': NODE_ENV === 'production' ? config.build.env : config.dev.env 46 | }), 47 | new webpack.optimize.ModuleConcatenationPlugin(), 48 | new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/), 49 | new UglifyJSPlugin({ 50 | exclude: /\.min\.js$/, 51 | cache: true, 52 | parallel: true 53 | }), 54 | new webpack.DllPlugin({ 55 | context: __dirname, 56 | path: path.join(__dirname, '[name]-manifest.json'), 57 | name: '[name]_[hash]_dll', 58 | }) 59 | ] 60 | }; 61 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const os = require('os'); 3 | const path = require('path') 4 | const utils = require('./utils') 5 | const webpack = require('webpack') 6 | const config = require('../config') 7 | const merge = require('webpack-merge') 8 | const baseWebpackConfig = require('./webpack.base.conf') 9 | const CopyWebpackPlugin = require('copy-webpack-plugin') 10 | const HtmlWebpackPlugin = require('html-webpack-plugin') 11 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 12 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 13 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 14 | 15 | const webpackConfig = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ 18 | sourceMap: config.build.productionSourceMap, 19 | extract: true 20 | }) 21 | }, 22 | devtool: config.build.productionSourceMap ? '#hidden-source-map' : false, 23 | output: { 24 | path: config.build.assetsRoot, 25 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 27 | }, 28 | plugins: [ 29 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 30 | new webpack.DefinePlugin({ 31 | 'process.env': config.build.env 32 | }), 33 | new webpack.optimize.ModuleConcatenationPlugin(), 34 | 35 | // extract css into its own file 36 | new ExtractTextPlugin({ 37 | filename: utils.assetsPath('css/[name].[contenthash].css'), 38 | allChunks: true 39 | }), 40 | // Compress extracted CSS. We are using this plugin so that possible 41 | // duplicated CSS from different components can be deduped. 42 | new OptimizeCSSPlugin({ 43 | cssProcessorOptions: { 44 | safe: true 45 | } 46 | }), 47 | new UglifyJSPlugin({ 48 | exclude: /\.min\.js$/, 49 | cache: true, 50 | parallel: true, 51 | sourceMap: true 52 | }), 53 | 54 | // keep module.id stable when vender modules does not change 55 | new webpack.HashedModuleIdsPlugin(), 56 | // split vendor js into its own file 57 | new webpack.optimize.CommonsChunkPlugin({ 58 | name: 'vendor', 59 | chunks: ['oj', 'admin'], 60 | minChunks: 2 61 | // minChunks: function (module) { 62 | // any required modules inside node_modules are extracted to vendor 63 | // return ( 64 | // module.resource && 65 | // /\.js$/.test(module.resource) && 66 | // module.resource.indexOf( 67 | // path.join(__dirname, '../node_modules') 68 | // ) === 0 69 | // ) 70 | // } 71 | }), 72 | // extract webpack runtime and module manifest to its own file in order to 73 | // prevent vendor hash from being updated whenever app bundle is updated 74 | new webpack.optimize.CommonsChunkPlugin({ 75 | name: 'manifest', 76 | chunks: ['vendor'] 77 | }), 78 | // copy custom static assets 79 | new CopyWebpackPlugin([ 80 | { 81 | from: path.resolve(__dirname, '../static'), 82 | to: config.build.assetsSubDirectory, 83 | ignore: ['.*'] 84 | } 85 | ]), 86 | // generate dist index.html with correct asset hash for caching. 87 | // you can customize output by editing /index.html 88 | // see https://github.com/ampedandwired/html-webpack-plugin 89 | // oj 90 | new HtmlWebpackPlugin({ 91 | filename: config.build.ojIndex, 92 | template: config.build.ojTemplate, 93 | chunks: ['manifest', 'vendor', 'oj'], 94 | inject: true, 95 | minify: { 96 | removeComments: true, 97 | collapseWhitespace: true, 98 | removeAttributeQuotes: true 99 | // more options: 100 | // https://github.com/kangax/html-minifier#options-quick-reference 101 | } 102 | }), 103 | // admin 104 | new HtmlWebpackPlugin({ 105 | filename: config.build.adminIndex, 106 | template: config.build.adminTemplate, 107 | chunks: ['manifest', 'vendor', 'admin'], 108 | inject: true, 109 | minify: { 110 | removeComments: true, 111 | collapseWhitespace: true, 112 | removeAttributeQuotes: true 113 | // more options: 114 | // https://github.com/kangax/html-minifier#options-quick-reference 115 | } 116 | }) 117 | ] 118 | }) 119 | 120 | if (config.build.productionGzip) { 121 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 122 | 123 | webpackConfig.plugins.push( 124 | new CompressionWebpackPlugin({ 125 | asset: '[path].gz[query]', 126 | algorithm: 'gzip', 127 | test: new RegExp( 128 | '\\.(' + 129 | config.build.productionGzipExtensions.join('|') + 130 | ')$' 131 | ), 132 | threshold: 10240, 133 | minRatio: 0.8 134 | }) 135 | ) 136 | } 137 | 138 | if (config.build.bundleAnalyzerReport) { 139 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 140 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 141 | } 142 | 143 | module.exports = webpackConfig 144 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | let date = require('moment')().format('YYYYMMDD') 2 | let commit = require('child_process').execSync('git rev-parse HEAD').toString().slice(0, 5) 3 | let version = `"${date}-${commit}"` 4 | 5 | console.log(`current version is ${version}`) 6 | 7 | module.exports = { 8 | NODE_ENV: '"development"', 9 | VERSION: version, 10 | USE_SENTRY: '0' 11 | } 12 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.1.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | const commonProxy = { 7 | onProxyReq: (proxyReq, req, res) => { 8 | proxyReq.setHeader('Referer', process.env.TARGET) 9 | }, 10 | target: process.env.TARGET, 11 | changeOrigin: true 12 | } 13 | 14 | module.exports = { 15 | build: { 16 | env: require('./prod.env'), 17 | ojIndex: path.resolve(__dirname, '../dist/index.html'), 18 | ojTemplate: path.resolve(__dirname, '../src/pages/oj/index.html'), 19 | adminIndex: path.resolve(__dirname, '../dist/admin/index.html'), 20 | adminTemplate: path.resolve(__dirname, '../src/pages/admin/index.html'), 21 | assetsRoot: path.resolve(__dirname, '../dist'), 22 | assetsSubDirectory: 'static', 23 | assetsPublicPath: '/__STATIC_CDN_HOST__/', 24 | productionSourceMap: process.env.USE_SENTRY === '1', 25 | // Gzip off by default as many popular static hosts such as 26 | // Surge or Netlify already gzip all static assets for you. 27 | // Before setting to `true`, make sure to: 28 | // npm install --save-dev compression-webpack-plugin 29 | productionGzip: false, 30 | productionGzipExtensions: ['js', 'css'], 31 | // Run the build command with an extra argument to 32 | // View the bundle analyzer report after build finishes: 33 | // `npm run build --report` 34 | // Set to `true` or `false` to always turn it on or off 35 | bundleAnalyzerReport: process.env.npm_config_report 36 | }, 37 | dev: { 38 | env: require('./dev.env'), 39 | port: process.env.PORT || 8080, 40 | autoOpenBrowser: true, 41 | assetsSubDirectory: 'static', 42 | assetsPublicPath: '/', 43 | proxyTable: { 44 | "/api": commonProxy, 45 | "/public": commonProxy 46 | }, 47 | // CSS Sourcemaps off by default because relative paths are "buggy" 48 | // with this option, according to the CSS-Loader README 49 | // (https://github.com/webpack/css-loader#sourcemaps) 50 | // In our experience, they generally work as expected, 51 | // just be aware of this issue when enabling this option. 52 | cssSourceMap: false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"production"', 6 | }) 7 | -------------------------------------------------------------------------------- /deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6.11-alpine 2 | 3 | RUN apk add --no-cache nginx git python build-base 4 | 5 | VOLUME [ "/OJ_FE", "/var/log/nginx/", "/data/avatar"] 6 | EXPOSE 80 7 | 8 | CMD ["/bin/sh", "/OJ_FE/deploy/run.sh"] 9 | -------------------------------------------------------------------------------- /deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | 3 | # Set number of worker processes automatically based on number of CPU cores. 4 | worker_processes auto; 5 | 6 | # Enables the use of JIT for regular expressions to speed-up their processing. 7 | pcre_jit on; 8 | 9 | # Configures default error logger. 10 | error_log /var/log/nginx/nginx_error.log warn; 11 | 12 | daemon off; 13 | 14 | # set pid path 15 | pid /tmp/nginx.pid; 16 | 17 | # Includes files with directives to load dynamic modules. 18 | include /etc/nginx/modules/*.conf; 19 | 20 | 21 | events { 22 | # The maximum number of simultaneous connections that can be opened by 23 | # a worker process. 24 | worker_connections 1024; 25 | } 26 | 27 | http { 28 | # Includes mapping of file name extensions to MIME types of responses 29 | # and defines the default type. 30 | include /etc/nginx/mime.types; 31 | default_type application/octet-stream; 32 | 33 | # Name servers used to resolve names of upstream servers into addresses. 34 | # It's also needed when using tcpsocket and udpsocket in Lua modules. 35 | #resolver 208.67.222.222 208.67.220.220; 36 | 37 | # Don't tell nginx version to clients. 38 | server_tokens off; 39 | 40 | # Specifies the maximum accepted body size of a client request, as 41 | # indicated by the request header Content-Length. If the stated content 42 | # length is greater than this size, then the client receives the HTTP 43 | # error code 413. Set to 0 to disable. 44 | client_max_body_size 100m; 45 | 46 | # Timeout for keep-alive connections. Server will close connections after 47 | # this time. 48 | keepalive_timeout 10; 49 | 50 | # Sendfile copies data between one FD and other from within the kernel, 51 | # which is more efficient than read() + write(). 52 | sendfile on; 53 | 54 | # Don't buffer data-sends (disable Nagle algorithm). 55 | # Good for sending frequent small bursts of data in real time. 56 | tcp_nodelay on; 57 | 58 | # Causes nginx to attempt to send its HTTP response head in one packet, 59 | # instead of using partial frames. 60 | #tcp_nopush on; 61 | 62 | 63 | # Path of the file with Diffie-Hellman parameters for EDH ciphers. 64 | #ssl_dhparam /etc/ssl/nginx/dh2048.pem; 65 | 66 | # Specifies that our cipher suits should be preferred over client ciphers. 67 | ssl_prefer_server_ciphers on; 68 | 69 | # Enables a shared SSL cache with size that can hold around 8000 sessions. 70 | ssl_session_cache shared:SSL:2m; 71 | 72 | 73 | # Enable gzipping of responses. 74 | gzip on; 75 | gzip_types application/javascript text/css; 76 | 77 | # Set the Vary HTTP header as defined in the RFC 2616. 78 | gzip_vary on; 79 | 80 | # Enable checking the existence of precompressed files. 81 | #gzip_static on; 82 | 83 | 84 | # Specifies the main log format. 85 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 86 | '$status $body_bytes_sent "$http_referer" ' 87 | '"$http_user_agent" "$http_x_forwarded_for"'; 88 | 89 | # Sets the path, format, and configuration for a buffered log write. 90 | # access_log /var/log/nginx/access.log main; 91 | access_log off; 92 | 93 | server { 94 | listen 80 default_server; 95 | server_name _; 96 | 97 | location /public { 98 | root /app/data; 99 | } 100 | location /api { 101 | proxy_pass http://oj-backend:8080; 102 | proxy_set_header X-Real-IP $remote_addr; 103 | proxy_set_header Host $http_host; 104 | client_max_body_size 200M; 105 | } 106 | location /admin { 107 | root /app/dist/admin; 108 | try_files $uri $uri/ /index.html =404; 109 | } 110 | location / { 111 | root /app/dist; 112 | try_files $uri $uri/ /index.html =404; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /deploy/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | base=/OJ_FE 4 | 5 | build_vendor_dll() 6 | { 7 | if [ ! -e "${base}/build/vendor-manifest.json" ] 8 | then 9 | npm run build:dll 10 | fi 11 | } 12 | cd $base 13 | npm install --registry=https://registry.npm.taobao.org && \ 14 | build_vendor_dll && \ 15 | npm run build 16 | 17 | if [ $? -ne 0 ]; then 18 | echo "Build error, please check node version and package.json" 19 | exit 1 20 | fi 21 | 22 | exec nginx -c /OJ_FE/deploy/nginx.conf 23 | -------------------------------------------------------------------------------- /deploy/sentry_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DATE=`date +%Y%m%d` 4 | COMMIT=`git rev-parse HEAD` 5 | VERSION="$DATE-${COMMIT:0:5}" 6 | 7 | echo "Current version is $VERSION" 8 | 9 | if [ ! -z $USE_SENTRY ] && [ $USE_SENTRY == '1' ]; then 10 | 11 | # create new release according to `VERSION` 12 | docker run --rm -it -v $(pwd):/work getsentry/sentry-cli \ 13 | sentry-cli --auth-token $SENTRY_AUTH_TOKEN releases -o onlinejudge -p onlinejudgefe new $VERSION 14 | 15 | # upload js and source_maps 16 | docker run --rm -it -v $(pwd):/work getsentry/sentry-cli \ 17 | sentry-cli --auth-token $SENTRY_AUTH_TOKEN releases -o onlinejudge -p onlinejudgefe files $VERSION upload-sourcemaps ./dist/static/js 18 | fi 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onlinejudge", 3 | "version": "2.7.6", 4 | "description": "onlinejudge front end", 5 | "author": "zemal ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js", 11 | "build:dll": "webpack --config=build/webpack.dll.conf.js", 12 | "lint": "eslint --ext .js,.vue src" 13 | }, 14 | "dependencies": { 15 | "autoprefixer": "^7.1.2", 16 | "axios": "^0.18.0", 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 20 | "babel-plugin-transform-runtime": "^6.22.0", 21 | "babel-polyfill": "^6.26.0", 22 | "babel-preset-env": "^1.7.0", 23 | "babel-preset-stage-2": "^6.22.0", 24 | "browser-detect": "^0.2.27", 25 | "chalk": "^2.4.1", 26 | "copy-webpack-plugin": "^4.5.1", 27 | "css-loader": "^0.28.11", 28 | "echarts": "^3.8.5", 29 | "element-ui": "^2.3.7", 30 | "extract-text-webpack-plugin": "^3.0.0", 31 | "file-loader": "^1.1.11", 32 | "font-awesome": "^4.7.0", 33 | "glob": "^7.1.2", 34 | "highlight.js": "^9.12.0", 35 | "html-webpack-include-assets-plugin": "^1.0.4", 36 | "html-webpack-plugin": "^2.30.1", 37 | "iview": "^2.13.0", 38 | "katex": "^0.10.0", 39 | "less": "^3.8.1", 40 | "less-loader": "^4.1.0", 41 | "moment": "^2.22.1", 42 | "optimize-css-assets-webpack-plugin": "^3.2.0", 43 | "ora": "^1.2.0", 44 | "papaparse": "^4.4.0", 45 | "raven-js": "^3.25.0", 46 | "screenfull": "^3.3.2", 47 | "shelljs": "^0.8.2", 48 | "tar-simditor": "^3.0.5", 49 | "tar-simditor-markdown": "^1.2.3", 50 | "uglifyjs-webpack-plugin": "^1.2.5", 51 | "url-loader": "^0.5.8", 52 | "vue": "^2.5.16", 53 | "vue-analytics": "^5.10.4", 54 | "vue-clipboard2": "^0.2.1", 55 | "vue-codemirror-lite": "^1.0.4", 56 | "vue-cropper": "^0.4.6", 57 | "vue-echarts": "^2.6.0", 58 | "vue-i18n": "^7.7.0", 59 | "vue-loader": "^13.3.0", 60 | "vue-router": "^3.0.1", 61 | "vue-style-loader": "^3.0.1", 62 | "vue-template-compiler": "^2.5.16", 63 | "vuex": "^3.0.1", 64 | "vuex-router-sync": "^5.0.0", 65 | "webpack": "^3.6.0", 66 | "webpack-merge": "^4.1.2" 67 | }, 68 | "devDependencies": { 69 | "babel-eslint": "^7.1.1", 70 | "babel-register": "^6.22.0", 71 | "connect-history-api-fallback": "^1.3.0", 72 | "eslint": "^3.19.0", 73 | "eslint-config-standard": "^10.2.1", 74 | "eslint-friendly-formatter": "^3.0.0", 75 | "eslint-loader": "^1.7.1", 76 | "eslint-plugin-html": "^3.0.0", 77 | "eslint-plugin-import": "^2.11.0", 78 | "eslint-plugin-node": "^5.2.0", 79 | "eslint-plugin-promise": "^3.7.0", 80 | "eslint-plugin-standard": "^3.1.0", 81 | "eventsource-polyfill": "^0.9.6", 82 | "express": "^4.16.3", 83 | "friendly-errors-webpack-plugin": "^1.7.0", 84 | "http-proxy-middleware": "^0.18.0", 85 | "opn": "^5.3.0", 86 | "portfinder": "^1.0.13", 87 | "rimraf": "^2.6.0", 88 | "semver": "^5.5.0", 89 | "webpack-bundle-analyzer": "^2.11.2", 90 | "webpack-dev-middleware": "^1.12.0", 91 | "webpack-hot-middleware": "^2.22.1" 92 | }, 93 | "engines": { 94 | "node": ">= 4.0.0", 95 | "npm": ">= 3.0.0" 96 | }, 97 | "browserslist": [ 98 | "> 1%", 99 | "last 2 versions", 100 | "not ie <= 8" 101 | ] 102 | } 103 | -------------------------------------------------------------------------------- /src/assets/Cup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QingdaoU/OnlineJudgeFE/44fa4744c21ad5098c21b1c5d974f4413a171319/src/assets/Cup.png -------------------------------------------------------------------------------- /src/i18n/admin/en-US.js: -------------------------------------------------------------------------------- 1 | export const m = { 2 | // SideMenu.vue 3 | Dashboard: 'Dashboard', 4 | General: 'General', 5 | User: 'User', 6 | Announcement: 'Announcement', 7 | System_Config: 'System Config', 8 | Judge_Server: 'Judge Server', 9 | Prune_Test_Case: 'Prune Test Case', 10 | Problem: 'Problem', 11 | FromFile: 'From File', 12 | ToFile: 'To File', 13 | ShareSubmission: 'Share Submission', 14 | Problem_List: 'Problem List', 15 | Create_Problem: 'Create Problem', 16 | Export_Import_Problem: 'Export Or Import Problem', 17 | Contest: 'Contest', 18 | Contest_List: 'Contest List', 19 | Create_Contest: 'Create Contest', 20 | // User.vue 21 | User_User: 'User', 22 | Import_User: 'Import User', 23 | Generate_User: 'Generate User', 24 | // User.vue-dialog 25 | User_Info: 'User', 26 | User_Username: 'Username', 27 | User_Real_Name: 'Real Name', 28 | User_Email: 'Email', 29 | User_New_Password: 'New Password', 30 | User_Type: 'User Type', 31 | Problem_Permission: 'Problem Permission', 32 | Two_Factor_Auth: 'Two Factor Auth', 33 | Is_Disabled: 'Is Disabled', 34 | // Announcement.vue 35 | General_Announcement: 'Announcement', 36 | Announcement_Title: 'Title', 37 | Announcement_Content: 'Content', 38 | Announcement_visible: 'Visible', 39 | // Conf.vue 40 | SMTP_Config: 'SMTP Config', 41 | Server: 'Server', 42 | Port: 'Port', 43 | Email: 'Email', 44 | Password: 'Password', 45 | Website_Config: 'Web Config', 46 | Base_Url: 'Base Url', 47 | Name: 'Name', 48 | Shortcut: 'Shortcut', 49 | Footer: 'Footer', 50 | Allow_Register: 'Allow Register', 51 | Submission_List_Show_All: 'Submission List Show All', 52 | // JudgeServer.vue 53 | Judge_Server_Token: 'Judge Server Token', 54 | Judge_Server_Info: 'Judge Server', 55 | IP: 'IP', 56 | Judger_Version: 'Judger Version', 57 | Service_URL: 'Service URL', 58 | Last_Heartbeat: 'Last Heartbeat', 59 | Create_Time: 'Create Time', 60 | // PruneTestCase 61 | Test_Case_Prune_Test_Case: 'Prune Test Case', 62 | // Problem.vue 63 | Display_ID: 'Display ID', 64 | Title: 'Title', 65 | Description: 'Description', 66 | Input_Description: 'Input Description', 67 | Output_Description: 'Output Description', 68 | Time_Limit: 'Time Limit', 69 | Memory_limit: 'Memory limit', 70 | Difficulty: 'Difficulty', 71 | Visible: 'Visible', 72 | Languages: 'Languages', 73 | Input_Samples: 'Input Samples', 74 | Output_Samples: 'Output Samples', 75 | Add_Sample: 'Add Sample', 76 | Code_Template: 'Code_Template', 77 | Special_Judge: 'Special Judge', 78 | Use_Special_Judge: 'Use Special Judge', 79 | Special_Judge_Code: 'Special Judge Code', 80 | SPJ_language: 'SPJ language', 81 | Compile: 'Compile', 82 | TestCase: 'TestCase', 83 | IOMode: 'IO Mode', 84 | InputFileName: 'Input File Name', 85 | OutputFileName: 'Output File Name', 86 | Type: 'Type', 87 | Input: 'Input', 88 | Output: 'Output', 89 | Score: 'Score', 90 | Hint: 'Hint', 91 | Source: 'Source', 92 | Edit_Problem: 'Edit Problme', 93 | Add_Problme: 'Add Problem', 94 | High: 'High', 95 | Mid: 'Mid', 96 | Low: 'Low', 97 | Tag: 'Tag', 98 | New_Tag: 'New Tag', 99 | // ProblemList.vue 100 | Contest_Problem_List: 'Contest Problem List', 101 | // Contest.vue 102 | ContestTitle: 'Title', 103 | ContestDescription: 'Description', 104 | Contest_Start_Time: 'Start Time', 105 | Contest_End_Time: 'End Time', 106 | Contest_Password: 'Password', 107 | Contest_Rule_Type: 'Contest Rule Type', 108 | Real_Time_Rank: 'Real Time Rank', 109 | Contest_Status: 'Status', 110 | Allowed_IP_Ranges: 'Allowed IP Ranges', 111 | CIDR_Network: 'CIDR Network', 112 | // Dashboard.vue 113 | Last_Login: 'Last Login', 114 | System_Overview: 'System Overview', 115 | DashBoardJudge_Server: 'Judge Server', 116 | HTTPS_Status: 'HTTPS Status', 117 | Force_HTTPS: 'Force HTTPS', 118 | CDN_HOST: 'CDN HOST', 119 | // Login.vue 120 | Welcome_to_Login: 'Welcome to Login', 121 | GO: 'GO', 122 | username: 'username', 123 | password: 'password' 124 | } 125 | -------------------------------------------------------------------------------- /src/i18n/admin/zh-CN.js: -------------------------------------------------------------------------------- 1 | export const m = { 2 | // SideMenu.vue 3 | Dashboard: '仪表盘', 4 | General: '常用设置', 5 | User: '用户管理', 6 | Announcement: '公告管理', 7 | System_Config: '系统配置', 8 | Judge_Server: '判题服务器', 9 | Prune_Test_Case: '测试用例', 10 | Problem: '问题', 11 | FromFile: '读取文件', 12 | ToFile: '写入文件', 13 | ShareSubmission: '分享提交', 14 | Problem_List: '问题列表', 15 | Create_Problem: '增加题目', 16 | Export_Import_Problem: '导入导出题目', 17 | Contest: '比赛&练习', 18 | Contest_List: '比赛列表', 19 | Create_Contest: '创建比赛', 20 | // User.vue 21 | User_User: '用户', 22 | Import_User: '导入用户', 23 | Generate_User: '生成用户', 24 | // User.vue-dialog 25 | User_Info: '用户信息', 26 | User_Username: '用户名', 27 | User_Real_Name: '真实姓名', 28 | User_Email: '用户邮箱', 29 | User_New_Password: '用户密码', 30 | User_Type: '用户类型', 31 | Problem_Permission: '问题权限', 32 | Two_Factor_Auth: '双因素认证', 33 | Is_Disabled: '是否禁用', 34 | // Announcement.vue 35 | General_Announcement: '公告', 36 | Announcement_Title: '标题', 37 | Announcement_Content: '内容', 38 | Announcement_visible: '是否可见', 39 | // Conf.vue 40 | SMTP_Config: 'SMTP 设置', 41 | Server: '服务器', 42 | Port: '端口', 43 | Email: '邮箱', 44 | Password: '授权码', 45 | Website_Config: '网站设置', 46 | Base_Url: '基础 Url', 47 | Name: '名称', 48 | Shortcut: '简称', 49 | Footer: '页脚', 50 | Allow_Register: '是否允许注册', 51 | Submission_List_Show_All: '显示全部题目的提交', 52 | // JudgeServer.vue 53 | Judge_Server_Token: '判题服务器接口', 54 | Judge_Server_Info: '判题服务器', 55 | IP: 'IP', 56 | Judger_Version: '判题机版本', 57 | Service_URL: '服务器 URL', 58 | Last_Heartbeat: '上一次心跳', 59 | Create_Time: '创建时间', 60 | // PruneTestCase 61 | Test_Case_Prune_Test_Case: '精简测试用例', 62 | // Problem.vue 63 | Display_ID: '显示 ID', 64 | Title: '题目', 65 | Description: '描述', 66 | Input_Description: '输入描述', 67 | Output_Description: '输出描述', 68 | Time_Limit: '时间限制', 69 | Memory_limit: '内存限制', 70 | Difficulty: '难度', 71 | Visible: '是否可见', 72 | Languages: '可选编程语言', 73 | Input_Samples: '输入样例', 74 | Output_Samples: '输出样例', 75 | Add_Sample: '添加样例', 76 | Code_Template: '代码模板', 77 | Special_Judge: 'Special Judge', 78 | Use_Special_Judge: '使用 Special Judge', 79 | Special_Judge_Code: 'Special Judge 代码', 80 | SPJ_language: 'SPJ 语言', 81 | Compile: '编译', 82 | TestCase: '测试用例', 83 | IOMode: 'IO 类型', 84 | InputFileName: '输入文件名', 85 | OutputFileName: '输出文件名', 86 | Type: '测试类型', 87 | Input: '输入', 88 | Output: '输出', 89 | Score: '分数', 90 | Hint: '提示', 91 | Source: '来源', 92 | Edit_Problem: '编辑问题', 93 | Add_Problem: '添加问题', 94 | High: '高', 95 | Mid: '中', 96 | Low: '低', 97 | Tag: '标签', 98 | New_Tag: '新增标签', 99 | // ProblemList.vue 100 | Contest_Problem_List: '比赛问题列表', 101 | // Contest.vue 102 | ContestTitle: '标题', 103 | ContestDescription: '描述', 104 | Contest_Start_Time: '开始时间', 105 | Contest_End_Time: '结束时间', 106 | Contest_Password: '密码', 107 | Contest_Rule_Type: '规则', 108 | Real_Time_Rank: '实时排名', 109 | Contest_Status: '状态', 110 | Allowed_IP_Ranges: '允许的 IP 范围', 111 | CIDR_Network: 'CIDR 网络', 112 | // Dashboard.vue 113 | Last_Login: '最后登录状态', 114 | System_Overview: '系统状况', 115 | DashBoardJudge_Server: '判题服务器', 116 | HTTPS_Status: 'HTTPS 状态', 117 | Force_HTTPS: '强制使用 HTTPS', 118 | CDN_HOST: 'CDN 主机', 119 | // Login.vue 120 | Welcome_to_Login: '欢迎登录 OnlineJudge 后台管理系统', 121 | GO: '登录', 122 | username: '用户名', 123 | password: '密码' 124 | } 125 | -------------------------------------------------------------------------------- /src/i18n/admin/zh-TW.js: -------------------------------------------------------------------------------- 1 | export const m = { 2 | // SideMenu.vue 3 | Dashboard: '儀表板', 4 | General: '基本設定', 5 | User: '使用者管理', 6 | Announcement: '公告管理', 7 | System_Config: '系統設定', 8 | Judge_Server: 'Judge 伺服器', 9 | Prune_Test_Case: '測資', 10 | Problem: '試題', 11 | FromFile: '讀取檔案', 12 | ToFile: '寫入檔案', 13 | ShareSubmission: '分享提交', 14 | Problem_List: '試題列表', 15 | Create_Problem: '增加題目', 16 | Export_Import_Problem: '匯入匯出題目', 17 | Contest: '比賽', 18 | Contest_List: '比賽列表', 19 | Create_Contest: '建立比賽', 20 | // User.vue 21 | User_User: '使用者', 22 | Import_User: '匯入使用者', 23 | Generate_User: '生成使用者', 24 | // User.vue-dialog 25 | User_Info: '使用者資訊', 26 | User_Username: '使用者名稱', 27 | User_Real_Name: '真實姓名', 28 | User_Email: '使用者 E-mail', 29 | User_New_Password: '使用者密碼', 30 | User_Type: '帳號類型', 31 | Problem_Permission: '試題權限', 32 | Two_Factor_Auth: '兩步驟驗證', 33 | Is_Disabled: '是否禁用', 34 | // Announcement.vue 35 | General_Announcement: '公告', 36 | Announcement_Title: '標題', 37 | Announcement_Content: '內容', 38 | Announcement_visible: '是否可見', 39 | // Conf.vue 40 | SMTP_Config: 'SMTP 設定', 41 | Server: '伺服器', 42 | Port: '連接埠', 43 | Email: 'E-mail', 44 | Password: '密碼', 45 | Website_Config: '網站設定', 46 | Base_Url: 'Base Url', 47 | Name: '名稱', 48 | Shortcut: '簡稱', 49 | Footer: '頁尾', 50 | Allow_Register: '是否允許註冊', 51 | Submission_List_Show_All: '顯示全部題目的提交', 52 | // JudgeServer.vue 53 | Judge_Server_Token: 'Judge 伺服器 Token', 54 | Judge_Server_Info: 'Judge 伺服器', 55 | IP: 'IP', 56 | Judger_Version: 'Judge 版本', 57 | Service_URL: '伺服器 URL', 58 | Last_Heartbeat: '上一次活動訊號', 59 | Create_Time: '建立時間', 60 | // PruneTestCase 61 | Test_Case_Prune_Test_Case: '精簡測資', 62 | // Problem.vue 63 | Display_ID: '顯示 ID', 64 | Title: '題目', 65 | Description: '描述', 66 | Input_Description: '輸入描述', 67 | Output_Description: '輸出描述', 68 | Time_Limit: '時間限制', 69 | Memory_limit: '記憶體限制', 70 | Difficulty: '難度', 71 | Visible: '是否可見', 72 | Languages: '可選程式語言', 73 | Input_Samples: '輸入範例', 74 | Output_Samples: '輸出範例', 75 | Add_Sample: '加入範例', 76 | Code_Template: '程式碼模板', 77 | Special_Judge: 'Special Judge', 78 | Use_Special_Judge: '使用 Special Judge', 79 | Special_Judge_Code: 'Special Judge Code', 80 | SPJ_language: 'SPJ language', 81 | Compile: '編譯', 82 | TestCase: '測資', 83 | IOMode: 'IO 類型', 84 | InputFileName: '輸入檔名', 85 | OutputFileName: '輸出檔名', 86 | Type: '測試類型', 87 | Input: '輸入', 88 | Output: '輸出', 89 | Score: '分數', 90 | Hint: '提示', 91 | Source: '來源', 92 | // Contest.vue 93 | ContestTitle: '標題', 94 | ContestDescription: '描述', 95 | Contest_Start_Time: '開始時間', 96 | Contest_End_Time: '結束時間', 97 | Contest_Password: '密碼', 98 | Contest_Rule_Type: '規則', 99 | Real_Time_Rank: '即時排名', 100 | Contest_Status: '狀態', 101 | Allowed_IP_Ranges: '允許的 IP 範圍', 102 | CIDR_Network: 'CIDR Network', 103 | // Dashboard.vue 104 | Last_Login: '最後登入狀態', 105 | System_Overview: '系統狀況', 106 | DashBoardJudge_Server: 'Judge 伺服器', 107 | HTTPS_Status: 'HTTPS 狀態', 108 | Force_HTTPS: '強制 HTTPS', 109 | CDN_HOST: 'CDN HOST' 110 | } 111 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | // ivew UI 4 | import ivenUS from 'iview/dist/locale/en-US' 5 | import ivzhCN from 'iview/dist/locale/zh-CN' 6 | import ivzhTW from 'iview/dist/locale/zh-TW' 7 | // element UI 8 | import elenUS from 'element-ui/lib/locale/lang/en' 9 | import elzhCN from 'element-ui/lib/locale/lang/zh-CN' 10 | import elzhTW from 'element-ui/lib/locale/lang/zh-TW' 11 | 12 | Vue.use(VueI18n) 13 | 14 | const languages = [ 15 | {value: 'en-US', label: 'English', iv: ivenUS, el: elenUS}, 16 | {value: 'zh-CN', label: '简体中文', iv: ivzhCN, el: elzhCN}, 17 | {value: 'zh-TW', label: '繁體中文', iv: ivzhTW, el: elzhTW} 18 | ] 19 | const messages = {} 20 | 21 | // combine admin and oj 22 | for (let lang of languages) { 23 | let locale = lang.value 24 | let m = require(`./oj/${locale}`).m 25 | Object.assign(m, require(`./admin/${locale}`).m) 26 | let ui = Object.assign(lang.iv, lang.el) 27 | messages[locale] = Object.assign({m: m}, ui) 28 | } 29 | // load language packages 30 | export default new VueI18n({ 31 | locale: 'en-US', 32 | messages: messages 33 | }) 34 | 35 | export {languages} 36 | -------------------------------------------------------------------------------- /src/pages/admin/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 31 | -------------------------------------------------------------------------------- /src/pages/admin/components/Accordion.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 77 | -------------------------------------------------------------------------------- /src/pages/admin/components/CodeMirror.vue: -------------------------------------------------------------------------------- 1 | 4 | 63 | 64 | 74 | -------------------------------------------------------------------------------- /src/pages/admin/components/KatexEditor.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /src/pages/admin/components/Panel.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 39 | 81 | 97 | -------------------------------------------------------------------------------- /src/pages/admin/components/ScreenFull.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | 44 | 51 | -------------------------------------------------------------------------------- /src/pages/admin/components/SideMenu.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 49 | 50 | 73 | -------------------------------------------------------------------------------- /src/pages/admin/components/Simditor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 72 | 73 | 75 | -------------------------------------------------------------------------------- /src/pages/admin/components/TopNav.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /src/pages/admin/components/btn/Cancel.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | -------------------------------------------------------------------------------- /src/pages/admin/components/btn/IconBtn.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/pages/admin/components/btn/Save.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | -------------------------------------------------------------------------------- /src/pages/admin/components/infoCard.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 47 | 48 | 83 | -------------------------------------------------------------------------------- /src/pages/admin/components/simditor-file-upload.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import Simditor from 'tar-simditor' 4 | import * as $ from 'jquery' 5 | 6 | var UploadFile, 7 | __hasProp = {}.hasOwnProperty, 8 | __extends = function (child, parent) { 9 | for (var key in parent) { 10 | if (__hasProp.call(parent, key)) child[key] = parent[key]; 11 | } 12 | 13 | function ctor() { 14 | this.constructor = child; 15 | } 16 | 17 | ctor.prototype = parent.prototype; 18 | child.prototype = new ctor(); 19 | child.__super__ = parent.prototype; 20 | return child; 21 | }, 22 | __slice = [].slice; 23 | 24 | UploadFile = (function (_super) { 25 | __extends(UploadFile, _super); 26 | 27 | UploadFile.i18n = { 28 | 'zh-CN': { 29 | uploadfile: '上传文件' 30 | }, 31 | 'en-US': { 32 | uploadfile: 'upload file' 33 | } 34 | }; 35 | 36 | UploadFile.prototype.name = 'uploadfile'; 37 | 38 | UploadFile.prototype.icon = 'upload'; 39 | 40 | function UploadFile() { 41 | var args; 42 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 43 | UploadFile.__super__.constructor.apply(this, args); 44 | this._initUpload(); 45 | } 46 | 47 | UploadFile.prototype._initUpload = function () { 48 | this.input = $('', { 49 | type: 'file', 50 | style: 'position:absolute;top:0;right:0;height:100%;width:100%;opacity:0;filter:alpha(opacity=0);cursor:pointer;' 51 | }).prependTo(this.el) 52 | var _this = this; 53 | this.el.on('click mousedown', 'input[type=file]', function (e) { 54 | return e.stopPropagation(); 55 | }).on('change', 'input[type=file]', function (e) { 56 | 57 | var formData = new FormData(); 58 | formData.append('file', this.files[0]); 59 | $.ajax({ 60 | url: '/api/admin/upload_file', 61 | type: 'POST', 62 | cache: false, 63 | data: formData, 64 | processData: false, 65 | contentType: false 66 | }).done(function (res) { 67 | if (!res.success) { 68 | alert("upload file failed") 69 | } else { 70 | let link = '' + res.file_name + '' 71 | _this.editor.setValue(_this.editor.getValue() + link) 72 | } 73 | }).fail(function (res) { 74 | alert("upload file failed") 75 | }); 76 | }); 77 | } 78 | 79 | return UploadFile; 80 | 81 | })(Simditor.Button); 82 | 83 | Simditor.Toolbar.addButton(UploadFile); 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/pages/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OnlineJudge 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/pages/admin/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import Vue from 'vue' 3 | import App from './App.vue' 4 | import store from '@/store' 5 | import i18n from '@/i18n' 6 | import Element from 'element-ui' 7 | import 'element-ui/lib/theme-chalk/index.css' 8 | 9 | import filters from '@/utils/filters' 10 | import router from './router' 11 | import { GOOGLE_ANALYTICS_ID } from '@/utils/constants' 12 | import VueAnalytics from 'vue-analytics' 13 | import katex from '@/plugins/katex' 14 | 15 | import Panel from './components/Panel.vue' 16 | import IconBtn from './components/btn/IconBtn.vue' 17 | import Save from './components/btn/Save.vue' 18 | import Cancel from './components/btn/Cancel.vue' 19 | import './style.less' 20 | 21 | // register global utility filters. 22 | Object.keys(filters).forEach(key => { 23 | Vue.filter(key, filters[key]) 24 | }) 25 | 26 | Vue.use(VueAnalytics, { 27 | id: GOOGLE_ANALYTICS_ID, 28 | router 29 | }) 30 | Vue.use(katex) 31 | Vue.component(IconBtn.name, IconBtn) 32 | Vue.component(Panel.name, Panel) 33 | Vue.component(Save.name, Save) 34 | Vue.component(Cancel.name, Cancel) 35 | 36 | Vue.use(Element, { 37 | i18n: (key, value) => i18n.t(key, value) 38 | }) 39 | 40 | Vue.prototype.$error = (msg) => { 41 | Vue.prototype.$message({'message': msg, 'type': 'error'}) 42 | } 43 | 44 | Vue.prototype.$warning = (msg) => { 45 | Vue.prototype.$message({'message': msg, 'type': 'warning'}) 46 | } 47 | 48 | Vue.prototype.$success = (msg) => { 49 | if (!msg) { 50 | Vue.prototype.$message({'message': 'Succeeded', 'type': 'success'}) 51 | } else { 52 | Vue.prototype.$message({'message': msg, 'type': 'success'}) 53 | } 54 | } 55 | 56 | new Vue(Vue.util.extend({router, store, i18n}, App)).$mount('#app') 57 | -------------------------------------------------------------------------------- /src/pages/admin/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | // 引入 view 组件 4 | import { Announcement, Conf, Contest, ContestList, Home, JudgeServer, Login, 5 | Problem, ProblemList, User, PruneTestCase, Dashboard, ProblemImportOrExport } from './views' 6 | Vue.use(VueRouter) 7 | 8 | export default new VueRouter({ 9 | mode: 'history', 10 | base: '/admin/', 11 | scrollBehavior: () => ({y: 0}), 12 | routes: [ 13 | { 14 | path: '/login', 15 | name: 'login', 16 | component: Login 17 | }, 18 | { 19 | path: '/', 20 | component: Home, 21 | children: [ 22 | { 23 | path: '', 24 | name: 'dashboard', 25 | component: Dashboard 26 | }, 27 | { 28 | path: '/announcement', 29 | name: 'announcement', 30 | component: Announcement 31 | }, 32 | { 33 | path: '/user', 34 | name: 'user', 35 | component: User 36 | }, 37 | { 38 | path: '/conf', 39 | name: 'conf', 40 | component: Conf 41 | }, 42 | { 43 | path: '/judge-server', 44 | name: 'judge-server', 45 | component: JudgeServer 46 | }, 47 | { 48 | path: '/prune-test-case', 49 | name: 'prune-test-case', 50 | component: PruneTestCase 51 | }, 52 | { 53 | path: '/problems', 54 | name: 'problem-list', 55 | component: ProblemList 56 | }, 57 | { 58 | path: '/problem/create', 59 | name: 'create-problem', 60 | component: Problem 61 | }, 62 | { 63 | path: '/problem/edit/:problemId', 64 | name: 'edit-problem', 65 | component: Problem 66 | }, 67 | { 68 | path: '/problem/batch_ops', 69 | name: 'problem_batch_ops', 70 | component: ProblemImportOrExport 71 | }, 72 | { 73 | path: '/contest/create', 74 | name: 'create-contest', 75 | component: Contest 76 | }, 77 | { 78 | path: '/contest', 79 | name: 'contest-list', 80 | component: ContestList 81 | }, 82 | { 83 | path: '/contest/:contestId/edit', 84 | name: 'edit-contest', 85 | component: Contest 86 | }, 87 | { 88 | path: '/contest/:contestId/announcement', 89 | name: 'contest-announcement', 90 | component: Announcement 91 | }, 92 | { 93 | path: '/contest/:contestId/problems', 94 | name: 'contest-problem-list', 95 | component: ProblemList 96 | }, 97 | { 98 | path: '/contest/:contestId/problem/create', 99 | name: 'create-contest-problem', 100 | component: Problem 101 | }, 102 | { 103 | path: '/contest/:contestId/problem/:problemId/edit', 104 | name: 'edit-contest-problem', 105 | component: Problem 106 | } 107 | ] 108 | }, 109 | { 110 | path: '*', redirect: '/login' 111 | } 112 | ] 113 | }) 114 | -------------------------------------------------------------------------------- /src/pages/admin/style.less: -------------------------------------------------------------------------------- 1 | [class^="el-icon-fa"], [class*=" el-icon-fa"] { 2 | font-family: FontAwesome !important; 3 | font-style: normal; 4 | font-weight: normal; 5 | line-height: 1; 6 | display: inline-block; 7 | text-rendering: auto; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | @import url("../../../node_modules/font-awesome/less/font-awesome"); 13 | @fa-css-prefix: el-icon-fa; 14 | -------------------------------------------------------------------------------- /src/pages/admin/views/Home.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 78 | 79 | 154 | -------------------------------------------------------------------------------- /src/pages/admin/views/general/JudgeServer.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 124 | -------------------------------------------------------------------------------- /src/pages/admin/views/general/Login.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | 60 | 83 | -------------------------------------------------------------------------------- /src/pages/admin/views/general/PruneTestCase.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 80 | 81 | 84 | -------------------------------------------------------------------------------- /src/pages/admin/views/index.js: -------------------------------------------------------------------------------- 1 | import Dashboard from './general/Dashboard.vue' 2 | import Announcement from './general/Announcement.vue' 3 | import User from './general/User.vue' 4 | import Conf from './general/Conf.vue' 5 | import JudgeServer from './general/JudgeServer.vue' 6 | import PruneTestCase from './general/PruneTestCase.vue' 7 | import Problem from './problem/Problem.vue' 8 | import ProblemList from './problem/ProblemList.vue' 9 | import ContestList from './contest/ContestList.vue' 10 | import Contest from './contest/Contest.vue' 11 | import Login from './general/Login.vue' 12 | import Home from './Home.vue' 13 | import ProblemImportOrExport from './problem/ImportAndExport.vue' 14 | 15 | export { 16 | Announcement, User, Conf, JudgeServer, Problem, ProblemList, Contest, 17 | ContestList, Login, Home, PruneTestCase, Dashboard, ProblemImportOrExport 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/admin/views/problem/AddPublicProblem.vue: -------------------------------------------------------------------------------- 1 | 44 | 106 | 113 | -------------------------------------------------------------------------------- /src/pages/admin/views/problem/ImportAndExport.vue: -------------------------------------------------------------------------------- 1 | 96 | 172 | 173 | 176 | -------------------------------------------------------------------------------- /src/pages/oj/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 58 | 59 | 103 | -------------------------------------------------------------------------------- /src/pages/oj/components/CodeMirror.vue: -------------------------------------------------------------------------------- 1 | 38 | 168 | 169 | 181 | 182 | 191 | -------------------------------------------------------------------------------- /src/pages/oj/components/Highlight.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 35 | 36 | 46 | -------------------------------------------------------------------------------- /src/pages/oj/components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 129 | 130 | 176 | -------------------------------------------------------------------------------- /src/pages/oj/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 50 | 51 | 57 | 58 | 63 | -------------------------------------------------------------------------------- /src/pages/oj/components/Panel.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 42 | 43 | 68 | -------------------------------------------------------------------------------- /src/pages/oj/components/mixins/emitter.js: -------------------------------------------------------------------------------- 1 | function broadcast (componentName, eventName, params) { 2 | this.$children.forEach(child => { 3 | const name = child.$options.name 4 | 5 | if (name === componentName) { 6 | child.$emit.apply(child, [eventName].concat(params)) 7 | } else { 8 | // todo 如果 params 是空数组,接收到的会是 undefined 9 | broadcast.apply(child, [componentName, eventName].concat([params])) 10 | } 11 | }) 12 | } 13 | 14 | export default { 15 | methods: { 16 | dispatch (componentName, eventName, params) { 17 | let parent = this.$parent || this.$root 18 | let name = parent.$options.name 19 | 20 | while (parent && (!name || name !== componentName)) { 21 | parent = parent.$parent 22 | 23 | if (parent) { 24 | name = parent.$options.name 25 | } 26 | } 27 | if (parent) { 28 | parent.$emit.apply(parent, [eventName].concat(params)) 29 | } 30 | }, 31 | broadcast (componentName, eventName, params) { 32 | broadcast.call(this, componentName, eventName, params) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/oj/components/mixins/form.js: -------------------------------------------------------------------------------- 1 | import api from '@oj/api' 2 | 3 | export default { 4 | data () { 5 | return { 6 | captchaSrc: '' 7 | } 8 | }, 9 | methods: { 10 | validateForm (formName) { 11 | return new Promise((resolve, reject) => { 12 | this.$refs[formName].validate(valid => { 13 | if (!valid) { 14 | this.$error('please validate the error fields') 15 | } else { 16 | resolve(valid) 17 | } 18 | }) 19 | }) 20 | }, 21 | getCaptchaSrc () { 22 | api.getCaptcha().then(res => { 23 | this.captchaSrc = res.data.data 24 | }) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/oj/components/mixins/index.js: -------------------------------------------------------------------------------- 1 | import Emitter from './emitter' 2 | import ProblemMixin from './problem' 3 | import FormMixin from './form' 4 | 5 | export {Emitter, ProblemMixin, FormMixin} 6 | -------------------------------------------------------------------------------- /src/pages/oj/components/mixins/problem.js: -------------------------------------------------------------------------------- 1 | import utils from '@/utils/utils' 2 | 3 | export default { 4 | data () { 5 | return { 6 | statusColumn: false 7 | } 8 | }, 9 | methods: { 10 | getACRate (ACCount, TotalCount) { 11 | return utils.getACRate(ACCount, TotalCount) 12 | }, 13 | addStatusColumn (tableColumns, dataProblems) { 14 | // 已添加过直接返回 15 | if (this.statusColumn) return 16 | // 只在有做题记录时才添加column 17 | let needAdd = dataProblems.some((item, index) => { 18 | if (item.my_status !== null && item.my_status !== undefined) { 19 | return true 20 | } 21 | }) 22 | if (!needAdd) { 23 | return 24 | } 25 | tableColumns.splice(0, 0, { 26 | width: 60, 27 | title: ' ', 28 | render: (h, params) => { 29 | let status = params.row.my_status 30 | if (status === null || status === undefined) { 31 | return undefined 32 | } 33 | return h('Icon', { 34 | props: { 35 | type: status === 0 ? 'checkmark-round' : 'minus-round', 36 | size: '16' 37 | }, 38 | style: { 39 | color: status === 0 ? '#19be6b' : '#ed3f14' 40 | } 41 | }) 42 | } 43 | }) 44 | this.statusColumn = true 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/oj/components/verticalMenu/verticalMenu-item.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | 32 | 66 | -------------------------------------------------------------------------------- /src/pages/oj/components/verticalMenu/verticalMenu.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 17 | -------------------------------------------------------------------------------- /src/pages/oj/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OnlineJudge 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/pages/oj/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import Vue from 'vue' 3 | import App from './App.vue' 4 | import router from './router' 5 | import store from '@/store' 6 | import i18n from '@/i18n' 7 | import VueClipboard from 'vue-clipboard2' 8 | import VueAnalytics from 'vue-analytics' 9 | import { GOOGLE_ANALYTICS_ID } from '@/utils/constants' 10 | 11 | import iView from 'iview' 12 | import 'iview/dist/styles/iview.css' 13 | 14 | import Panel from '@oj/components/Panel.vue' 15 | import VerticalMenu from '@oj/components/verticalMenu/verticalMenu.vue' 16 | import VerticalMenuItem from '@oj/components/verticalMenu/verticalMenu-item.vue' 17 | import '@/styles/index.less' 18 | 19 | import highlight from '@/plugins/highlight' 20 | import katex from '@/plugins/katex' 21 | import filters from '@/utils/filters.js' 22 | 23 | import ECharts from 'vue-echarts/components/ECharts.vue' 24 | import 'echarts/lib/chart/bar' 25 | import 'echarts/lib/chart/line' 26 | import 'echarts/lib/chart/pie' 27 | import 'echarts/lib/component/title' 28 | import 'echarts/lib/component/grid' 29 | import 'echarts/lib/component/dataZoom' 30 | import 'echarts/lib/component/legend' 31 | import 'echarts/lib/component/tooltip' 32 | import 'echarts/lib/component/toolbox' 33 | import 'echarts/lib/component/markPoint' 34 | 35 | // register global utility filters. 36 | Object.keys(filters).forEach(key => { 37 | Vue.filter(key, filters[key]) 38 | }) 39 | 40 | Vue.config.productionTip = false 41 | Vue.use(iView, { 42 | i18n: (key, value) => i18n.t(key, value) 43 | }) 44 | 45 | Vue.use(VueClipboard) 46 | Vue.use(highlight) 47 | Vue.use(katex) 48 | Vue.use(VueAnalytics, { 49 | id: GOOGLE_ANALYTICS_ID, 50 | router 51 | }) 52 | 53 | Vue.component('ECharts', ECharts) 54 | Vue.component(VerticalMenu.name, VerticalMenu) 55 | Vue.component(VerticalMenuItem.name, VerticalMenuItem) 56 | Vue.component(Panel.name, Panel) 57 | 58 | // 注册全局消息提示 59 | Vue.prototype.$Message.config({ 60 | duration: 2 61 | }) 62 | Vue.prototype.$error = (s) => Vue.prototype.$Message.error(s) 63 | Vue.prototype.$info = (s) => Vue.prototype.$Message.info(s) 64 | Vue.prototype.$success = (s) => Vue.prototype.$Message.success(s) 65 | 66 | new Vue(Vue.util.extend({router, store, i18n}, App)).$mount('#app') 67 | -------------------------------------------------------------------------------- /src/pages/oj/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import routes from './routes' 4 | import storage from '@/utils/storage' 5 | import {STORAGE_KEY} from '@/utils/constants' 6 | import {sync} from 'vuex-router-sync' 7 | import {types, default as store} from '../../../store' 8 | 9 | Vue.use(VueRouter) 10 | 11 | const router = new VueRouter({ 12 | mode: 'history', 13 | scrollBehavior (to, from, savedPosition) { 14 | if (savedPosition) { 15 | return savedPosition 16 | } else { 17 | return {x: 0, y: 0} 18 | } 19 | }, 20 | routes 21 | }) 22 | 23 | // 全局身份确认 24 | router.beforeEach((to, from, next) => { 25 | Vue.prototype.$Loading.start() 26 | if (to.matched.some(record => record.meta.requiresAuth)) { 27 | if (!storage.get(STORAGE_KEY.AUTHED)) { 28 | Vue.prototype.$error('Please login first') 29 | store.commit(types.CHANGE_MODAL_STATUS, {mode: 'login', visible: true}) 30 | next({ 31 | name: 'home' 32 | }) 33 | } else { 34 | next() 35 | } 36 | } else { 37 | next() 38 | } 39 | }) 40 | 41 | router.afterEach((to, from, next) => { 42 | Vue.prototype.$Loading.finish() 43 | }) 44 | 45 | sync(store, router) 46 | 47 | export default router 48 | -------------------------------------------------------------------------------- /src/pages/oj/router/routes.js: -------------------------------------------------------------------------------- 1 | // all routes here. 2 | import { 3 | About, 4 | ACMRank, 5 | Announcements, 6 | ApplyResetPassword, 7 | FAQ, 8 | Home, 9 | Logout, 10 | NotFound, 11 | OIRank, 12 | Problem, 13 | ProblemList, 14 | ResetPassword, 15 | SubmissionDetails, 16 | SubmissionList, 17 | UserHome 18 | } from '../views' 19 | 20 | import * as Contest from '@oj/views/contest' 21 | import * as Setting from '@oj/views/setting' 22 | 23 | export default [ 24 | { 25 | name: 'home', 26 | path: '/', 27 | meta: {title: 'Home'}, 28 | component: Home 29 | }, 30 | { 31 | name: 'logout', 32 | path: '/logout', 33 | meta: {title: 'Logout'}, 34 | component: Logout 35 | }, 36 | { 37 | name: 'apply-reset-password', 38 | path: '/apply-reset-password', 39 | meta: {title: 'Apply Reset Password'}, 40 | component: ApplyResetPassword 41 | }, 42 | { 43 | name: 'reset-password', 44 | path: '/reset-password/:token', 45 | meta: {title: 'Reset Password'}, 46 | component: ResetPassword 47 | }, 48 | { 49 | name: 'problem-list', 50 | path: '/problem', 51 | meta: {title: 'Problem List'}, 52 | component: ProblemList 53 | }, 54 | { 55 | name: 'problem-details', 56 | path: '/problem/:problemID', 57 | meta: {title: 'Problem Details'}, 58 | component: Problem 59 | }, 60 | { 61 | name: 'submission-list', 62 | path: '/status', 63 | meta: {title: 'Submission List'}, 64 | component: SubmissionList 65 | }, 66 | { 67 | name: 'submission-details', 68 | path: '/status/:id/', 69 | meta: {title: 'Submission Details'}, 70 | component: SubmissionDetails 71 | }, 72 | { 73 | name: 'contest-list', 74 | path: '/contest', 75 | meta: {title: 'Contest List'}, 76 | component: Contest.ContestList 77 | }, 78 | { 79 | name: 'contest-details', 80 | path: '/contest/:contestID/', 81 | component: Contest.ContestDetails, 82 | meta: {title: 'Contest Details'}, 83 | children: [ 84 | { 85 | name: 'contest-submission-list', 86 | path: 'submissions', 87 | component: SubmissionList 88 | }, 89 | { 90 | name: 'contest-problem-list', 91 | path: 'problems', 92 | component: Contest.ContestProblemList 93 | }, 94 | { 95 | name: 'contest-problem-details', 96 | path: 'problem/:problemID/', 97 | component: Problem 98 | }, 99 | { 100 | name: 'contest-announcement-list', 101 | path: 'announcements', 102 | component: Announcements 103 | }, 104 | { 105 | name: 'contest-rank', 106 | path: 'rank', 107 | component: Contest.ContestRank 108 | }, 109 | { 110 | name: 'acm-helper', 111 | path: 'helper', 112 | component: Contest.ACMContestHelper 113 | } 114 | ] 115 | }, 116 | { 117 | name: 'acm-rank', 118 | path: '/acm-rank', 119 | meta: {title: 'ACM Rankings'}, 120 | component: ACMRank 121 | }, 122 | { 123 | name: 'oi-rank', 124 | path: '/oi-rank', 125 | meta: {title: 'OI Rankings'}, 126 | component: OIRank 127 | }, 128 | { 129 | name: 'user-home', 130 | path: '/user-home', 131 | component: UserHome, 132 | meta: {requiresAuth: true, title: 'User Home'} 133 | }, 134 | { 135 | path: '/setting', 136 | component: Setting.Settings, 137 | children: [ 138 | { 139 | name: 'default-setting', 140 | path: '', 141 | meta: {requiresAuth: true, title: 'Default Settings'}, 142 | component: Setting.ProfileSetting 143 | }, 144 | { 145 | name: 'profile-setting', 146 | path: 'profile', 147 | meta: {requiresAuth: true, title: 'Profile Settings'}, 148 | component: Setting.ProfileSetting 149 | }, 150 | { 151 | name: 'account-setting', 152 | path: 'account', 153 | meta: {requiresAuth: true, title: 'Account Settings'}, 154 | component: Setting.AccountSetting 155 | }, 156 | { 157 | name: 'security-setting', 158 | path: 'security', 159 | meta: {requiresAuth: true, title: 'Security Settings'}, 160 | component: Setting.SecuritySetting 161 | } 162 | ] 163 | }, 164 | { 165 | path: '/about', 166 | name: 'about', 167 | meta: {title: 'About'}, 168 | component: About 169 | }, 170 | { 171 | path: '/faq', 172 | name: 'faq', 173 | meta: {title: 'FAQ'}, 174 | component: FAQ 175 | }, 176 | { 177 | path: '*', 178 | meta: {title: '404'}, 179 | component: NotFound 180 | } 181 | ] 182 | -------------------------------------------------------------------------------- /src/pages/oj/views/contest/children/ContestProblemList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 96 | 97 | 99 | -------------------------------------------------------------------------------- /src/pages/oj/views/contest/children/ContestRank.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 40 | 42 | -------------------------------------------------------------------------------- /src/pages/oj/views/contest/children/contestRankMixin.js: -------------------------------------------------------------------------------- 1 | import api from '@oj/api' 2 | import ScreenFull from '@admin/components/ScreenFull.vue' 3 | import { mapGetters, mapState } from 'vuex' 4 | import { types } from '@/store' 5 | import { CONTEST_STATUS } from '@/utils/constants' 6 | 7 | export default { 8 | components: { 9 | ScreenFull 10 | }, 11 | methods: { 12 | getContestRankData (page = 1, refresh = false) { 13 | let offset = (page - 1) * this.limit 14 | if (this.showChart && !refresh) { 15 | this.$refs.chart.showLoading({maskColor: 'rgba(250, 250, 250, 0.8)'}) 16 | } 17 | let params = { 18 | offset, 19 | limit: this.limit, 20 | contest_id: this.$route.params.contestID, 21 | force_refresh: this.forceUpdate ? '1' : '0' 22 | } 23 | api.getContestRank(params).then(res => { 24 | if (this.showChart && !refresh) { 25 | this.$refs.chart.hideLoading() 26 | } 27 | this.total = res.data.data.total 28 | if (page === 1) { 29 | this.applyToChart(res.data.data.results.slice(0, 10)) 30 | } 31 | this.applyToTable(res.data.data.results) 32 | }) 33 | }, 34 | handleAutoRefresh (status) { 35 | if (status === true) { 36 | this.refreshFunc = setInterval(() => { 37 | this.page = 1 38 | this.getContestRankData(1, true) 39 | }, 10000) 40 | } else { 41 | clearInterval(this.refreshFunc) 42 | } 43 | } 44 | }, 45 | computed: { 46 | ...mapGetters(['isContestAdmin']), 47 | ...mapState({ 48 | 'contest': state => state.contest.contest, 49 | 'contestProblems': state => state.contest.contestProblems 50 | }), 51 | showChart: { 52 | get () { 53 | return this.$store.state.contest.itemVisible.chart 54 | }, 55 | set (value) { 56 | this.$store.commit(types.CHANGE_CONTEST_ITEM_VISIBLE, {chart: value}) 57 | } 58 | }, 59 | showMenu: { 60 | get () { 61 | return this.$store.state.contest.itemVisible.menu 62 | }, 63 | set (value) { 64 | this.$store.commit(types.CHANGE_CONTEST_ITEM_VISIBLE, {menu: value}) 65 | this.$nextTick(() => { 66 | if (this.showChart) { 67 | this.$refs.chart.resize() 68 | } 69 | this.$refs.tableRank.handleResize() 70 | }) 71 | } 72 | }, 73 | showRealName: { 74 | get () { 75 | return this.$store.state.contest.itemVisible.realName 76 | }, 77 | set (value) { 78 | this.$store.commit(types.CHANGE_CONTEST_ITEM_VISIBLE, {realName: value}) 79 | if (value) { 80 | this.columns.splice(2, 0, { 81 | title: 'RealName', 82 | align: 'center', 83 | width: 150, 84 | render: (h, {row}) => { 85 | return h('span', row.user.real_name) 86 | } 87 | }) 88 | } else { 89 | this.columns.splice(2, 1) 90 | } 91 | } 92 | }, 93 | forceUpdate: { 94 | get () { 95 | return this.$store.state.contest.forceUpdate 96 | }, 97 | set (value) { 98 | this.$store.commit(types.CHANGE_RANK_FORCE_UPDATE, {value: value}) 99 | } 100 | }, 101 | limit: { 102 | get () { 103 | return this.$store.state.contest.rankLimit 104 | }, 105 | set (value) { 106 | this.$store.commit(types.CHANGE_CONTEST_RANK_LIMIT, {rankLimit: value}) 107 | } 108 | }, 109 | refreshDisabled () { 110 | return this.contest.status === CONTEST_STATUS.ENDED 111 | } 112 | }, 113 | beforeDestroy () { 114 | clearInterval(this.refreshFunc) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/pages/oj/views/contest/index.js: -------------------------------------------------------------------------------- 1 | const ContestList = () => import(/* webpackChunkName: "contest" */ './ContestList.vue') 2 | const ContestDetails = () => import(/* webpackChunkName: "contest" */ './ContestDetail.vue') 3 | const ContestProblemList = () => import(/* webpackChunkName: "contest" */ './children/ContestProblemList.vue') 4 | const ContestRank = () => import(/* webpackChunkName: "contest" */ './children/ContestRank.vue') 5 | const ACMContestHelper = () => import(/* webpackChunkName: "contest" */ './children/ACMHelper.vue') 6 | 7 | export {ContestDetails, ContestList, ContestProblemList, ContestRank, ACMContestHelper} 8 | -------------------------------------------------------------------------------- /src/pages/oj/views/general/404.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | 32 | 90 | -------------------------------------------------------------------------------- /src/pages/oj/views/general/Announcements.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 113 | 114 | 168 | -------------------------------------------------------------------------------- /src/pages/oj/views/general/Home.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 70 | 71 | 89 | -------------------------------------------------------------------------------- /src/pages/oj/views/help/About.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 58 | 59 | 78 | -------------------------------------------------------------------------------- /src/pages/oj/views/help/FAQ.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 37 | 38 | 58 | -------------------------------------------------------------------------------- /src/pages/oj/views/index.js: -------------------------------------------------------------------------------- 1 | import ProblemList from './problem/ProblemList.vue' 2 | import Logout from './user/Logout.vue' 3 | import UserHome from './user/UserHome.vue' 4 | import About from './help/About.vue' 5 | import FAQ from './help/FAQ.vue' 6 | import NotFound from './general/404.vue' 7 | import Home from './general/Home.vue' 8 | import Announcements from './general/Announcements.vue' 9 | 10 | // Grouping Components in the Same Chunk 11 | const SubmissionList = () => import(/* webpackChunkName: "submission" */ '@oj/views/submission/SubmissionList.vue') 12 | const SubmissionDetails = () => import(/* webpackChunkName: "submission" */ '@oj/views/submission/SubmissionDetails.vue') 13 | 14 | const ACMRank = () => import(/* webpackChunkName: "userRank" */ '@oj/views/rank/ACMRank.vue') 15 | const OIRank = () => import(/* webpackChunkName: "userRank" */ '@oj/views/rank/OIRank.vue') 16 | 17 | const ApplyResetPassword = () => import(/* webpackChunkName: "password" */ '@oj/views/user/ApplyResetPassword.vue') 18 | const ResetPassword = () => import(/* webpackChunkName: "password" */ '@oj/views/user/ResetPassword.vue') 19 | 20 | const Problem = () => import(/* webpackChunkName: "Problem" */ '@oj/views/problem/Problem.vue') 21 | 22 | export { 23 | Home, NotFound, Announcements, 24 | Logout, UserHome, About, FAQ, 25 | ProblemList, Problem, 26 | ACMRank, OIRank, 27 | SubmissionList, SubmissionDetails, 28 | ApplyResetPassword, ResetPassword 29 | } 30 | /* 组件导出分为两类, 一类常用的直接导出,另一类诸如Login, Logout等用懒加载,懒加载不在此处导出 31 | * 在对应的route内加载 32 | * 见https://router.vuejs.org/en/advanced/lazy-loading.html 33 | */ 34 | -------------------------------------------------------------------------------- /src/pages/oj/views/problem/chartData.js: -------------------------------------------------------------------------------- 1 | const pieColorMap = { 2 | 'AC': {color: '#19be6b'}, 3 | 'WA': {color: '#ed3f14'}, 4 | 'TLE': {color: '#ff9300'}, 5 | 'MLE': {color: '#f7de00'}, 6 | 'RE': {color: '#ff6104'}, 7 | 'CE': {color: '#80848f'}, 8 | 'PAC': {color: '#2d8cf0'} 9 | } 10 | 11 | function getItemColor (obj) { 12 | return pieColorMap[obj.name].color 13 | } 14 | 15 | const pie = { 16 | legend: { 17 | left: 'center', 18 | top: '10', 19 | orient: 'horizontal', 20 | data: ['AC', 'WA'] 21 | }, 22 | series: [ 23 | { 24 | name: 'Summary', 25 | type: 'pie', 26 | radius: '80%', 27 | center: ['50%', '55%'], 28 | itemStyle: { 29 | normal: {color: getItemColor} 30 | }, 31 | data: [ 32 | {value: 0, name: 'WA'}, 33 | {value: 0, name: 'AC'} 34 | ], 35 | label: { 36 | normal: { 37 | position: 'inner', 38 | show: true, 39 | formatter: '{b}: {c}\n {d}%', 40 | textStyle: { 41 | fontWeight: 'bold' 42 | } 43 | } 44 | } 45 | } 46 | ] 47 | } 48 | 49 | const largePie = { 50 | legend: { 51 | left: 'center', 52 | top: 53 | '10', 54 | orient: 55 | 'horizontal', 56 | itemGap: 57 | 20, 58 | data: 59 | ['AC', 'RE', 'WA', 'TLE', 'PAC', 'MLE'] 60 | }, 61 | series: [ 62 | { 63 | name: 'Detail', 64 | type: 'pie', 65 | radius: ['45%', '70%'], 66 | center: ['50%', '55%'], 67 | itemStyle: { 68 | normal: {color: getItemColor} 69 | }, 70 | data: [ 71 | {value: 0, name: 'RE'}, 72 | {value: 0, name: 'WA'}, 73 | {value: 0, name: 'TLE'}, 74 | {value: 0, name: 'AC'}, 75 | {value: 0, name: 'MLE'}, 76 | {value: 0, name: 'PAC'} 77 | ], 78 | label: { 79 | normal: { 80 | formatter: '{b}: {c}\n {d}%' 81 | } 82 | }, 83 | labelLine: { 84 | normal: {} 85 | } 86 | }, 87 | { 88 | name: 'Summary', 89 | type: 'pie', 90 | radius: '30%', 91 | center: ['50%', '55%'], 92 | itemStyle: { 93 | normal: {color: getItemColor} 94 | }, 95 | data: [ 96 | {value: '0', name: 'WA'}, 97 | {value: 0, name: 'AC', selected: true} 98 | ], 99 | label: { 100 | normal: { 101 | position: 'inner', 102 | formatter: '{b}: {c}\n {d}%' 103 | } 104 | } 105 | } 106 | ] 107 | } 108 | 109 | export { pie, largePie } 110 | -------------------------------------------------------------------------------- /src/pages/oj/views/rank/OIRank.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 182 | 183 | 190 | -------------------------------------------------------------------------------- /src/pages/oj/views/setting/Settings.vue: -------------------------------------------------------------------------------- 1 | 35 | 53 | 54 | 137 | 138 | 151 | -------------------------------------------------------------------------------- /src/pages/oj/views/setting/index.js: -------------------------------------------------------------------------------- 1 | const Settings = () => import(/* webpackChunkName: "setting" */ './Settings.vue') 2 | const ProfileSetting = () => import(/* webpackChunkName: "setting" */ './children/ProfileSetting.vue') 3 | const SecuritySetting = () => import(/* webpackChunkName: "setting" */ './children/SecuritySetting.vue') 4 | const AccountSetting = () => import(/* webpackChunkName: "setting" */ './children/AccountSetting.vue') 5 | 6 | export {Settings, ProfileSetting, SecuritySetting, AccountSetting} 7 | -------------------------------------------------------------------------------- /src/pages/oj/views/user/ApplyResetPassword.vue: -------------------------------------------------------------------------------- 1 | 40 | 102 | 103 | 116 | -------------------------------------------------------------------------------- /src/pages/oj/views/user/Login.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 113 | 114 | 128 | -------------------------------------------------------------------------------- /src/pages/oj/views/user/Logout.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 18 | -------------------------------------------------------------------------------- /src/pages/oj/views/user/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 113 | 141 | -------------------------------------------------------------------------------- /src/plugins/highlight.js: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js/lib/highlight' 2 | import cpp from 'highlight.js/lib/languages/cpp' 3 | import python from 'highlight.js/lib/languages/python' 4 | import java from 'highlight.js/lib/languages/java' 5 | import 'highlight.js/styles/atom-one-light.css' 6 | 7 | hljs.registerLanguage('cpp', cpp) 8 | hljs.registerLanguage('java', java) 9 | hljs.registerLanguage('python', python) 10 | 11 | export default { 12 | install (Vue, options) { 13 | Vue.directive('highlight', { 14 | deep: true, 15 | bind: function (el, binding) { 16 | // on first bind, highlight all targets 17 | Array.from(el.querySelectorAll('code')).forEach((target) => { 18 | // if a value is directly assigned to the directive, use this 19 | // instead of the element content. 20 | if (binding.value) { 21 | target.textContent = binding.value 22 | } 23 | hljs.highlightBlock(target) 24 | }) 25 | }, 26 | componentUpdated: function (el, binding) { 27 | // after an update, re-fill the content and then highlight 28 | Array.from(el.querySelectorAll('code')).forEach((target) => { 29 | if (binding.value) { 30 | target.textContent = binding.value 31 | } 32 | hljs.highlightBlock(target) 33 | }) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/plugins/katex.js: -------------------------------------------------------------------------------- 1 | import 'katex' 2 | import renderMathInElement from 'katex/contrib/auto-render/auto-render' 3 | import 'katex/dist/katex.min.css' 4 | 5 | function _ () { 6 | } 7 | 8 | const defaultOptions = { 9 | errorCallback: _, 10 | throwOnError: false, 11 | delimiters: [ 12 | {left: '$$', right: '$$', display: true}, 13 | {left: '$', right: '$', display: false}, 14 | {left: '\\[', right: '\\]', display: true}, 15 | {left: '\\(', right: '\\)', display: false} 16 | ] 17 | } 18 | 19 | function render (el, binding) { 20 | let options = {} 21 | if (binding.value) { 22 | options = binding.value.options || {} 23 | } 24 | Object.assign(options, defaultOptions) 25 | renderMathInElement(el, options) 26 | } 27 | 28 | export default { 29 | install: function (Vue, options) { 30 | Vue.directive('katex', { 31 | bind: render, 32 | componentUpdated: render 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import user from './modules/user' 4 | import contest from './modules/contest' 5 | import api from '@oj/api' 6 | import types from './types' 7 | 8 | Vue.use(Vuex) 9 | const debug = process.env.NODE_ENV !== 'production' 10 | 11 | const rootState = { 12 | website: {}, 13 | modalStatus: { 14 | mode: 'login', // or 'register', 15 | visible: false 16 | } 17 | } 18 | 19 | const rootGetters = { 20 | 'website' (state) { 21 | return state.website 22 | }, 23 | 'modalStatus' (state) { 24 | return state.modalStatus 25 | } 26 | } 27 | 28 | const rootMutations = { 29 | [types.UPDATE_WEBSITE_CONF] (state, payload) { 30 | state.website = payload.websiteConfig 31 | }, 32 | [types.CHANGE_MODAL_STATUS] (state, {mode, visible}) { 33 | if (mode !== undefined) { 34 | state.modalStatus.mode = mode 35 | } 36 | if (visible !== undefined) { 37 | state.modalStatus.visible = visible 38 | } 39 | } 40 | } 41 | 42 | const rootActions = { 43 | getWebsiteConfig ({commit}) { 44 | api.getWebsiteConf().then(res => { 45 | commit(types.UPDATE_WEBSITE_CONF, { 46 | websiteConfig: res.data.data 47 | }) 48 | }) 49 | }, 50 | changeModalStatus ({commit}, payload) { 51 | commit(types.CHANGE_MODAL_STATUS, payload) 52 | }, 53 | changeDomTitle ({commit, state}, payload) { 54 | if (payload && payload.title) { 55 | window.document.title = state.website.website_name_shortcut + ' | ' + payload.title 56 | } else { 57 | window.document.title = state.website.website_name_shortcut + ' | ' + state.route.meta.title 58 | } 59 | } 60 | } 61 | 62 | export default new Vuex.Store({ 63 | modules: { 64 | user, 65 | contest 66 | }, 67 | state: rootState, 68 | getters: rootGetters, 69 | mutations: rootMutations, 70 | actions: rootActions, 71 | strict: debug 72 | }) 73 | 74 | export { types } 75 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import types from '../types' 2 | import api from '@oj/api' 3 | import storage from '@/utils/storage' 4 | import i18n from '@/i18n' 5 | import { STORAGE_KEY, USER_TYPE, PROBLEM_PERMISSION } from '@/utils/constants' 6 | 7 | const state = { 8 | profile: {} 9 | } 10 | 11 | const getters = { 12 | user: state => state.profile.user || {}, 13 | profile: state => state.profile, 14 | isAuthenticated: (state, getters) => { 15 | return !!getters.user.id 16 | }, 17 | isAdminRole: (state, getters) => { 18 | return getters.user.admin_type === USER_TYPE.ADMIN || 19 | getters.user.admin_type === USER_TYPE.SUPER_ADMIN 20 | }, 21 | isSuperAdmin: (state, getters) => { 22 | return getters.user.admin_type === USER_TYPE.SUPER_ADMIN 23 | }, 24 | hasProblemPermission: (state, getters) => { 25 | return getters.user.problem_permission !== PROBLEM_PERMISSION.NONE 26 | } 27 | } 28 | 29 | const mutations = { 30 | [types.CHANGE_PROFILE] (state, {profile}) { 31 | state.profile = profile 32 | if (profile.language) { 33 | i18n.locale = profile.language 34 | } 35 | storage.set(STORAGE_KEY.AUTHED, !!profile.user) 36 | } 37 | } 38 | 39 | const actions = { 40 | getProfile ({commit}) { 41 | api.getUserInfo().then(res => { 42 | commit(types.CHANGE_PROFILE, { 43 | profile: res.data.data || {} 44 | }) 45 | }) 46 | }, 47 | clearProfile ({commit}) { 48 | commit(types.CHANGE_PROFILE, { 49 | profile: {} 50 | }) 51 | storage.clear() 52 | } 53 | } 54 | 55 | export default { 56 | state, 57 | getters, 58 | actions, 59 | mutations 60 | } 61 | -------------------------------------------------------------------------------- /src/store/types.js: -------------------------------------------------------------------------------- 1 | function keyMirror (obj) { 2 | if (obj instanceof Object) { 3 | var _obj = Object.assign({}, obj) 4 | var _keyArray = Object.keys(obj) 5 | _keyArray.forEach(key => { 6 | _obj[key] = key 7 | }) 8 | return _obj 9 | } 10 | } 11 | 12 | export default keyMirror({ 13 | 'CHANGE_PROFILE': null, 14 | 'CHANGE_MODAL_STATUS': null, 15 | 'UPDATE_WEBSITE_CONF': null, 16 | 17 | 'NOW': null, 18 | 'NOW_ADD_1S': null, 19 | 'CHANGE_CONTEST': null, 20 | 'CHANGE_CONTEST_PROBLEMS': null, 21 | 'CHANGE_CONTEST_ITEM_VISIBLE': null, 22 | 'CHANGE_RANK_FORCE_UPDATE': null, 23 | 'CHANGE_CONTEST_RANK_LIMIT': null, 24 | 'CONTEST_ACCESS': null, 25 | 'CLEAR_CONTEST': null 26 | }) 27 | -------------------------------------------------------------------------------- /src/styles/common.less: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #eee; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | background-color: #eee; 11 | min-width: 900px; 12 | } 13 | 14 | .flex-container { 15 | display: flex; 16 | width: 100%; 17 | max-width: 100%; 18 | justify-content: space-around; 19 | align-items: flex-start; 20 | flex-flow: row nowrap; 21 | } 22 | 23 | .section-title { 24 | font-size: 21px; 25 | font-weight: 500; 26 | padding-top: 10px; 27 | padding-bottom: 20px; 28 | line-height: 30px; 29 | } 30 | 31 | .separator { 32 | display: block; 33 | position: absolute; 34 | top: 0; 35 | bottom: 0; 36 | left: 50%; 37 | border: 1px dashed #eee; 38 | } 39 | 40 | .oj-captcha { 41 | display: flex; 42 | flex-wrap: nowrap; 43 | justify-content: space-between; 44 | width: 100%; 45 | height: 36px; 46 | .oj-captcha-code { 47 | flex: auto; 48 | } 49 | .oj-captcha-img { 50 | margin-left: 10px; 51 | padding: 3px; 52 | flex: initial; 53 | } 54 | } 55 | 56 | .oj-relative { 57 | position: relative; 58 | } 59 | 60 | a.emphasis{ 61 | color: #495060; 62 | &:hover { 63 | color: #2d8cf0; 64 | } 65 | } 66 | 67 | // for mathjax 68 | .MathJax { 69 | outline: 0; 70 | } 71 | 72 | .MathJax_Display { 73 | overflow-x: auto; 74 | overflow-y: hidden; 75 | } 76 | 77 | .markdown-body { 78 | @import './markdown.less'; 79 | } 80 | 81 | @keyframes fadeInUp { 82 | from { 83 | opacity: 0; 84 | transform: translate(0, 50px); 85 | } 86 | 87 | to { 88 | opacity: 1; 89 | transform: none; 90 | } 91 | } 92 | 93 | @keyframes fadeIn { 94 | from { 95 | opacity: 0; 96 | } 97 | 98 | to { 99 | opacity: 1; 100 | } 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './common.less'; 2 | @import './iview-custom.less'; 3 | -------------------------------------------------------------------------------- /src/styles/iview-custom.less: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100% !important; 3 | } 4 | .auto-resize { 5 | table { 6 | table-layout: auto !important; 7 | } 8 | } 9 | .ivu-table-wrapper { 10 | border: none; 11 | } 12 | 13 | .ivu-table td { 14 | border-bottom-color: #dddddd; 15 | } 16 | 17 | .ivu-card-head { 18 | border-bottom-width: 0; 19 | } 20 | 21 | .ivu-table td { 22 | border-bottom-color: #dddddd; 23 | } 24 | 25 | .ivu-table .first-ac { 26 | background-color: #33CC99; 27 | color: #3c763d; 28 | } 29 | 30 | .ivu-table .ac { 31 | background-color: #dff0d8;; 32 | color: #3c763d; 33 | } 34 | 35 | .ivu-table .wa { 36 | color: #a94442; 37 | background-color: #f2dede; 38 | } 39 | 40 | .ivu-modal-footer { 41 | border-top-width: 0; 42 | padding: 0 18px 20px 18px; 43 | } 44 | .ivu-modal-body { 45 | padding: 18px; 46 | } 47 | -------------------------------------------------------------------------------- /src/styles/markdown.less: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4 { 2 | color: #111111; 3 | font-weight: 400; 4 | } 5 | 6 | h1, h2, h3, h4, h5, p { 7 | margin-bottom: 15px; 8 | padding: 0; 9 | } 10 | 11 | h1 { 12 | font-size: 28px; 13 | } 14 | 15 | h2 { 16 | font-size: 24px; 17 | } 18 | 19 | h3 { 20 | font-size: 20px; 21 | } 22 | 23 | h4 { 24 | font-size: 18px; 25 | } 26 | 27 | h5 { 28 | font-size: 14px; 29 | } 30 | 31 | a { 32 | color: #0099ff; 33 | margin: 0; 34 | padding: 0; 35 | vertical-align: baseline; 36 | } 37 | 38 | ul, ol { 39 | padding: 0; 40 | margin: 10px 20px; 41 | } 42 | 43 | ul { 44 | list-style-type: disc; 45 | } 46 | 47 | ol { 48 | list-style-type: decimal; 49 | } 50 | 51 | li { 52 | line-height: 24px; 53 | } 54 | 55 | li ul, li ul { 56 | margin-left: 24px; 57 | } 58 | 59 | p, ul, ol { 60 | font-size: 16px; 61 | line-height: 24px; 62 | } 63 | 64 | pre { 65 | padding: 5px 10px; 66 | white-space: pre-wrap; 67 | margin-top: 15px; 68 | margin-bottom: 15px; 69 | background: #f8f8f9; 70 | border: 1px dashed #e9eaec; 71 | } 72 | 73 | code { 74 | font-size: 90%; 75 | padding: 2px 5px; 76 | margin: 0; 77 | background-color: rgba(27, 31, 35, 0.05); 78 | border-radius: 3px; 79 | line-height: 1.5; 80 | } 81 | 82 | pre > code { 83 | padding: 0; 84 | margin: 0; 85 | font-size: 100%; 86 | word-break: normal; 87 | white-space: pre; 88 | background: transparent; 89 | border: 0; 90 | } 91 | 92 | aside { 93 | display: block; 94 | float: right; 95 | width: 390px; 96 | } 97 | 98 | blockquote { 99 | border-left: 3px solid #bbbec4; 100 | padding-left: 10px; 101 | margin-top: 10px; 102 | margin-bottom: 10px; 103 | color: #7b7b7b; 104 | } 105 | 106 | hr { 107 | width: 540px; 108 | text-align: left; 109 | margin: 0 auto 0 0; 110 | color: #999; 111 | } 112 | 113 | table { 114 | border-collapse: collapse; 115 | margin: 1em 1em; 116 | border: 1px solid #ccc; 117 | thead { 118 | background-color: #eee; 119 | td { 120 | color: #666; 121 | } 122 | } 123 | td { 124 | padding: 0.5em 1em; 125 | border: 1px solid #ccc; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const JUDGE_STATUS = { 2 | '-2': { 3 | name: 'Compile Error', 4 | short: 'CE', 5 | color: 'yellow', 6 | type: 'warning' 7 | }, 8 | '-1': { 9 | name: 'Wrong Answer', 10 | short: 'WA', 11 | color: 'red', 12 | type: 'error' 13 | }, 14 | '0': { 15 | name: 'Accepted', 16 | short: 'AC', 17 | color: 'green', 18 | type: 'success' 19 | }, 20 | '1': { 21 | name: 'Time Limit Exceeded', 22 | short: 'TLE', 23 | color: 'red', 24 | type: 'error' 25 | }, 26 | '2': { 27 | name: 'Time Limit Exceeded', 28 | short: 'TLE', 29 | color: 'red', 30 | type: 'error' 31 | }, 32 | '3': { 33 | name: 'Memory Limit Exceeded', 34 | short: 'MLE', 35 | color: 'red', 36 | type: 'error' 37 | }, 38 | '4': { 39 | name: 'Runtime Error', 40 | short: 'RE', 41 | color: 'red', 42 | type: 'error' 43 | }, 44 | '5': { 45 | name: 'System Error', 46 | short: 'SE', 47 | color: 'red', 48 | type: 'error' 49 | }, 50 | '6': { 51 | name: 'Pending', 52 | color: 'yellow', 53 | type: 'warning' 54 | }, 55 | '7': { 56 | name: 'Judging', 57 | color: 'blue', 58 | type: 'info' 59 | }, 60 | '8': { 61 | name: 'Partial Accepted', 62 | short: 'PAC', 63 | color: 'blue', 64 | type: 'info' 65 | }, 66 | '9': { 67 | name: 'Submitting', 68 | color: 'yellow', 69 | type: 'warning' 70 | } 71 | } 72 | 73 | export const CONTEST_STATUS = { 74 | 'NOT_START': '1', 75 | 'UNDERWAY': '0', 76 | 'ENDED': '-1' 77 | } 78 | 79 | export const CONTEST_STATUS_REVERSE = { 80 | '1': { 81 | name: 'Not Started', 82 | color: 'yellow' 83 | }, 84 | '0': { 85 | name: 'Underway', 86 | color: 'green' 87 | }, 88 | '-1': { 89 | name: 'Ended', 90 | color: 'red' 91 | } 92 | } 93 | 94 | export const RULE_TYPE = { 95 | ACM: 'ACM', 96 | OI: 'OI' 97 | } 98 | 99 | export const CONTEST_TYPE = { 100 | PUBLIC: 'Public', 101 | PRIVATE: 'Password Protected' 102 | } 103 | 104 | export const USER_TYPE = { 105 | REGULAR_USER: 'Regular User', 106 | ADMIN: 'Admin', 107 | SUPER_ADMIN: 'Super Admin' 108 | } 109 | 110 | export const PROBLEM_PERMISSION = { 111 | NONE: 'None', 112 | OWN: 'Own', 113 | ALL: 'All' 114 | } 115 | 116 | export const STORAGE_KEY = { 117 | AUTHED: 'authed', 118 | PROBLEM_CODE: 'problemCode', 119 | languages: 'languages' 120 | } 121 | 122 | export function buildProblemCodeKey (problemID, contestID = null) { 123 | if (contestID) { 124 | return `${STORAGE_KEY.PROBLEM_CODE}_${contestID}_${problemID}` 125 | } 126 | return `${STORAGE_KEY.PROBLEM_CODE}_NaN_${problemID}` 127 | } 128 | 129 | export const GOOGLE_ANALYTICS_ID = 'UA-111499601-1' 130 | -------------------------------------------------------------------------------- /src/utils/filters.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import utils from './utils' 3 | import time from './time' 4 | 5 | // 友好显示时间 6 | function fromNow (time) { 7 | return moment(time * 3).fromNow() 8 | } 9 | 10 | export default { 11 | submissionMemory: utils.submissionMemoryFormat, 12 | submissionTime: utils.submissionTimeFormat, 13 | localtime: time.utcToLocal, 14 | fromNow: fromNow 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/sentry.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Raven from 'raven-js' 3 | import RavenVue from 'raven-js/plugins/vue' 4 | 5 | const options = { 6 | release: process.env.VERSION, 7 | ignoreUrls: [ 8 | // Chrome extensions 9 | /extensions\//i, 10 | /^chrome:\/\//i, 11 | // Firefox extensions 12 | /^resource:\/\//i, 13 | // Ignore Google flakiness 14 | /\/(gtm|ga|analytics)\.js/i 15 | ] 16 | } 17 | 18 | Raven 19 | .config('https://6234a51e61a743b089ed64c51d2f6ea9@sentry.io/258234', options) 20 | .addPlugin(RavenVue, Vue) 21 | .install() 22 | 23 | Raven.setUserContext({ 24 | version: process.env.VERSION, 25 | location: window.location 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | const localStorage = window.localStorage 2 | 3 | export default { 4 | name: 'storage', 5 | 6 | /** 7 | * save value(Object) to key 8 | * @param {string} key 键 9 | * @param {Object} value 值 10 | */ 11 | set (key, value) { 12 | localStorage.setItem(key, JSON.stringify(value)) 13 | }, 14 | 15 | /** 16 | * get value(Object) by key 17 | * @param {string} key 键 18 | * @return {Object} 19 | */ 20 | get (key) { 21 | return JSON.parse(localStorage.getItem(key)) || null 22 | }, 23 | 24 | /** 25 | * remove key from localStorage 26 | * @param {string} key 键 27 | */ 28 | remove (key) { 29 | localStorage.removeItem(key) 30 | }, 31 | /** 32 | * clear all 33 | */ 34 | clear () { 35 | localStorage.clear() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/time.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | // convert utc time to localtime 4 | function utcToLocal (utcDt, format = 'YYYY-M-D HH:mm:ss') { 5 | return moment.utc(utcDt).local().format(format) 6 | } 7 | 8 | // get duration from startTime to endTime, return like 3 days, 2 hours, one year .. 9 | function duration (startTime, endTime) { 10 | let start = moment(startTime) 11 | let end = moment(endTime) 12 | let duration = moment.duration(start.diff(end, 'seconds'), 'seconds') 13 | if (duration.days() !== 0) { 14 | return duration.humanize() 15 | } 16 | return Math.abs(duration.asHours().toFixed(1)) + ' hours' 17 | } 18 | 19 | function secondFormat (seconds) { 20 | let m = moment.duration(seconds, 'seconds') 21 | return Math.floor(m.asHours()) + ':' + m.minutes() + ':' + m.seconds() 22 | } 23 | 24 | export default { 25 | utcToLocal: utcToLocal, 26 | duration: duration, 27 | secondFormat: secondFormat 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import storage from '@/utils/storage' 3 | import { STORAGE_KEY } from '@/utils/constants' 4 | import ojAPI from '@oj/api' 5 | 6 | function submissionMemoryFormat (memory) { 7 | if (memory === undefined) return '--' 8 | // 1048576 = 1024 * 1024 9 | let t = parseInt(memory) / 1048576 10 | return String(t.toFixed(0)) + 'MB' 11 | } 12 | 13 | function submissionTimeFormat (time) { 14 | if (time === undefined) return '--' 15 | return time + 'ms' 16 | } 17 | 18 | function getACRate (acCount, totalCount) { 19 | let rate = totalCount === 0 ? 0.00 : (acCount / totalCount * 100).toFixed(2) 20 | return String(rate) + '%' 21 | } 22 | 23 | // 去掉值为空的项,返回object 24 | function filterEmptyValue (object) { 25 | let query = {} 26 | Object.keys(object).forEach(key => { 27 | if (object[key] || object[key] === 0 || object[key] === false) { 28 | query[key] = object[key] 29 | } 30 | }) 31 | return query 32 | } 33 | 34 | // 按指定字符数截断添加换行,非英文字符按指定字符的半数截断 35 | function breakLongWords (value, length = 16) { 36 | let re 37 | if (escape(value).indexOf('%u') === -1) { 38 | // 没有中文 39 | re = new RegExp('(.{' + length + '})', 'g') 40 | } else { 41 | // 中文字符 42 | re = new RegExp('(.{' + (length / 2 + 1) + '})', 'g') 43 | } 44 | return value.replace(re, '$1\n') 45 | } 46 | 47 | function downloadFile (url) { 48 | return new Promise((resolve, reject) => { 49 | Vue.prototype.$http.get(url, {responseType: 'blob'}).then(resp => { 50 | let headers = resp.headers 51 | if (headers['content-type'].indexOf('json') !== -1) { 52 | let fr = new window.FileReader() 53 | if (resp.data.error) { 54 | Vue.prototype.$error(resp.data.error) 55 | } else { 56 | Vue.prototype.$error('Invalid file format') 57 | } 58 | fr.onload = (event) => { 59 | let data = JSON.parse(event.target.result) 60 | if (data.error) { 61 | Vue.prototype.$error(data.data) 62 | } else { 63 | Vue.prototype.$error('Invalid file format') 64 | } 65 | } 66 | let b = new window.Blob([resp.data], {type: 'application/json'}) 67 | fr.readAsText(b) 68 | return 69 | } 70 | let link = document.createElement('a') 71 | link.href = window.URL.createObjectURL(new window.Blob([resp.data], {type: headers['content-type']})) 72 | link.download = (headers['content-disposition'] || '').split('filename=')[1] 73 | document.body.appendChild(link) 74 | link.click() 75 | link.remove() 76 | resolve() 77 | }).catch(() => {}) 78 | }) 79 | } 80 | 81 | function getLanguages () { 82 | return new Promise((resolve, reject) => { 83 | let languages = storage.get(STORAGE_KEY.languages) 84 | if (languages) { 85 | resolve(languages) 86 | } 87 | ojAPI.getLanguages().then(res => { 88 | let languages = res.data.data.languages 89 | storage.set(STORAGE_KEY.languages, languages) 90 | resolve(languages) 91 | }, err => { 92 | reject(err) 93 | }) 94 | }) 95 | } 96 | 97 | export default { 98 | submissionMemoryFormat: submissionMemoryFormat, 99 | submissionTimeFormat: submissionTimeFormat, 100 | getACRate: getACRate, 101 | filterEmptyValue: filterEmptyValue, 102 | breakLongWords: breakLongWords, 103 | downloadFile: downloadFile, 104 | getLanguages: getLanguages 105 | } 106 | -------------------------------------------------------------------------------- /static/css/loader.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes enter { 2 | 0% { 3 | opacity: 0; 4 | top: -10px; 5 | } 6 | 5% { 7 | opacity: 1; 8 | top: 0px; 9 | } 10 | 50.9% { 11 | opacity: 1; 12 | top: 0px; 13 | } 14 | 55.9% { 15 | opacity: 0; 16 | top: 10px; 17 | } 18 | } 19 | @keyframes enter { 20 | 0% { 21 | opacity: 0; 22 | top: -10px; 23 | } 24 | 5% { 25 | opacity: 1; 26 | top: 0px; 27 | } 28 | 50.9% { 29 | opacity: 1; 30 | top: 0px; 31 | } 32 | 55.9% { 33 | opacity: 0; 34 | top: 10px; 35 | } 36 | } 37 | @-moz-keyframes enter { 38 | 0% { 39 | opacity: 0; 40 | top: -10px; 41 | } 42 | 5% { 43 | opacity: 1; 44 | top: 0px; 45 | } 46 | 50.9% { 47 | opacity: 1; 48 | top: 0px; 49 | } 50 | 55.9% { 51 | opacity: 0; 52 | top: 10px; 53 | } 54 | } 55 | body { 56 | background: #f8f8f9; 57 | } 58 | 59 | #app-loader { 60 | position: absolute; 61 | left: 50%; 62 | top: 50%; 63 | margin-left: -27.5px; 64 | margin-top: -27.5px; 65 | } 66 | #app-loader .square { 67 | background: #2d8cf0; 68 | width: 15px; 69 | height: 15px; 70 | float: left; 71 | top: -10px; 72 | margin-right: 5px; 73 | margin-top: 5px; 74 | position: relative; 75 | opacity: 0; 76 | -webkit-animation: enter 6s infinite; 77 | animation: enter 6s infinite; 78 | } 79 | #app-loader .enter { 80 | top: 0px; 81 | opacity: 1; 82 | } 83 | #app-loader .square:nth-child(1) { 84 | -webkit-animation-delay: 1.8s; 85 | -moz-animation-delay: 1.8s; 86 | animation-delay: 1.8s; 87 | } 88 | #app-loader .square:nth-child(2) { 89 | -webkit-animation-delay: 2.1s; 90 | -moz-animation-delay: 2.1s; 91 | animation-delay: 2.1s; 92 | } 93 | #app-loader .square:nth-child(3) { 94 | -webkit-animation-delay: 2.4s; 95 | -moz-animation-delay: 2.4s; 96 | animation-delay: 2.4s; 97 | background: #ff9900; 98 | } 99 | #app-loader .square:nth-child(4) { 100 | -webkit-animation-delay: 0.9s; 101 | -moz-animation-delay: 0.9s; 102 | animation-delay: 0.9s; 103 | } 104 | #app-loader .square:nth-child(5) { 105 | -webkit-animation-delay: 1.2s; 106 | -moz-animation-delay: 1.2s; 107 | animation-delay: 1.2s; 108 | } 109 | #app-loader .square:nth-child(6) { 110 | -webkit-animation-delay: 1.5s; 111 | -moz-animation-delay: 1.5s; 112 | animation-delay: 1.5s; 113 | } 114 | #app-loader .square:nth-child(8) { 115 | -webkit-animation-delay: 0.3s; 116 | -moz-animation-delay: 0.3s; 117 | animation-delay: 0.3s; 118 | } 119 | #app-loader .square:nth-child(9) { 120 | -webkit-animation-delay: 0.6s; 121 | -moz-animation-delay: 0.6s; 122 | animation-delay: 0.6s; 123 | } 124 | #app-loader .clear { 125 | clear: both; 126 | } 127 | #app-loader .last { 128 | margin-right: 0; 129 | } 130 | --------------------------------------------------------------------------------