├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── nginx.conf ├── prod.env.js └── test.env.js ├── index.html ├── package.json ├── public ├── logo-120.png ├── logo-144.png ├── logo-152.png ├── logo-192.png ├── logo-384.png └── logo-48.png ├── src ├── assets │ ├── js │ │ └── bootstrap.min.js │ ├── logo.png │ └── stylesheets │ │ ├── app.css │ │ └── markdown.css ├── components │ ├── Footer.vue │ ├── Header.vue │ ├── Item.vue │ ├── ItemList.vue │ ├── NodeItem.vue │ ├── NodeList.vue │ ├── Pagination.vue │ ├── RepliesList.vue │ ├── ReplyItem.vue │ ├── Sider.vue │ ├── Spinner.vue │ └── TabHeader.vue ├── main.js ├── router.js ├── store │ ├── actions │ │ ├── api.js │ │ ├── node.js │ │ ├── reply.js │ │ └── topic.js │ ├── getters.js │ ├── index.js │ ├── modules │ │ ├── index.js │ │ ├── node.js │ │ ├── reply.js │ │ └── topic.js │ └── mutation-types.js └── views │ ├── App.vue │ ├── Dashboard.vue │ ├── Sites.vue │ ├── TopicDetail.vue │ ├── Wiki.vue │ ├── app.js │ └── createListView.js ├── static └── .gitkeep └── test ├── e2e ├── custom-assertions │ └── elementCount.js ├── nightwatch.conf.js ├── runner.js └── specs │ └── test.js └── unit ├── .eslintrc ├── index.js ├── karma.conf.js └── specs └── Hello.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false, 5 | "env": { 6 | "test": { 7 | "plugins": [ "istanbul" ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.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 | extends: 'airbnb-base', 8 | // required to lint *.vue files 9 | plugins: [ 10 | 'html' 11 | ], 12 | // check if imports actually resolve 13 | 'settings': { 14 | 'import/resolver': { 15 | 'webpack': { 16 | 'config': 'build/webpack.base.conf.js' 17 | } 18 | } 19 | }, 20 | // add your custom rules here 21 | 'rules': { 22 | // don't require .vue extension when importing 23 | 'import/extensions': ['error', 'always', { 24 | 'js': 'never', 25 | 'vue': 'never' 26 | }], 27 | // allow debugger during development 28 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | test/unit/coverage 6 | test/e2e/reports 7 | selenium-debug.log 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Ruby China 2 | 3 | > Ruby China 山寨版 (Vue框架) 4 | 5 | ## Demo 6 | 7 | https://hql123.github.io/ 8 | 9 | ## 项目简介 10 | 11 | 这个项目还是以Ruby China为范本搭建的模仿平台,Vue.js框架+Bootstrap框架开发,集成vue-cli + vuex + vue-router + vue-resource,大概还会尝试加入服务端渲染的SSR。这个项目我个人觉得挺适合Vuex的初学者尝试的,功能简单逻辑也不复杂,对于actions、 getters、mutations、modules的单向数据流模式应该都使用得挺清晰的。 12 | 13 | ## 关于项目目录 14 | 15 | 当初写 React 的 Ruby China 山寨版的时候有人提出了 components 是UI组件,功能主要是可复用,纯粹的渲染组件,尽量不掺杂vuex或redux的使用到这里面,我深以为然!于是在这个项目里面可以看到 components 里面所有的组件都是单纯的渲染可复用组件,在 views 的组件里面定义好 vuex 的 state 通过 props 传过去使用,这是一个好习惯呀! 16 | 17 | ## Build Setup 18 | 19 | ``` bash 20 | # install dependencies 21 | npm install 22 | 23 | # serve with hot reload at localhost:8080 24 | npm run dev 25 | 26 | # build for production with minification 27 | npm run build 28 | 29 | # run unit tests 30 | npm run unit 31 | 32 | # run e2e tests 33 | npm run e2e 34 | 35 | # run all tests 36 | npm test 37 | ``` 38 | 39 | ## nginx配置 40 | 41 | ```bash 42 | http { 43 | include mime.types; 44 | default_type application/octet-stream; 45 | server { 46 | listen 9000; 47 | server_name ruby-china.local; 48 | root ../ruby-china/dist/; //项目根目录 49 | index index.html; 50 | location ^~ /static/ { 51 | gzip_static on; 52 | expires max; 53 | add_header Cache-Control public; 54 | } 55 | location / { 56 | try_files $uri $uri/ /index.html; 57 | } 58 | location /api/v3{ 59 | proxy_pass https://ruby-china.org/api/v3; 60 | proxy_redirect off; 61 | proxy_buffering off; 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ##Vuex数据流 68 | 69 |

70 | 71 |

72 | 73 | ## 参考文献 74 | 75 | [Vue.js官方GitHub](https://github.com/vuejs) 76 | 77 | [Vue.js](https://cn.vuejs.org/) 78 | 79 | [Vuex](https://vuex.vuejs.org/zh-cn/) 80 | 81 | [vue-router 2](https://router.vuejs.org/zh-cn/) 82 | 83 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('./check-versions')() 3 | require('shelljs/global') 4 | env.NODE_ENV = 'production' 5 | 6 | var path = require('path') 7 | var config = require('../config') 8 | var ora = require('ora') 9 | var webpack = require('webpack') 10 | var webpackConfig = require('./webpack.prod.conf') 11 | 12 | console.log( 13 | ' Tip:\n' + 14 | ' Built files are meant to be served over an HTTP server.\n' + 15 | ' Opening index.html over file:// won\'t work.\n' 16 | ) 17 | 18 | var spinner = ora('building for production...') 19 | spinner.start() 20 | 21 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 22 | rm('-rf', assetsPath) 23 | mkdir('-p', assetsPath) 24 | cp('-R', 'static/*', assetsPath) 25 | 26 | webpack(webpackConfig, function (err, stats) { 27 | spinner.stop() 28 | if (err) throw err 29 | process.stdout.write(stats.toString({ 30 | colors: true, 31 | modules: false, 32 | children: false, 33 | chunks: false, 34 | chunkModules: false 35 | }) + '\n') 36 | }) 37 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver') 2 | var chalk = require('chalk') 3 | var packageConfig = require('../package.json') 4 | var exec = function (cmd) { 5 | return require('child_process') 6 | .execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | var config = require('../config') 3 | if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 4 | var path = require('path') 5 | var express = require('express') 6 | 7 | var webpack = require('webpack') 8 | var opn = require('opn') 9 | var proxyMiddleware = require('http-proxy-middleware') 10 | var webpackConfig = process.env.NODE_ENV === 'testing' 11 | ? require('./webpack.prod.conf') 12 | : require('./webpack.dev.conf') 13 | const resolve = file => path.resolve(__dirname, file) 14 | 15 | // default port where dev server listens for incoming traffic 16 | var port = process.env.PORT || config.dev.port 17 | // Define HTTP proxies to your custom API backend 18 | // https://github.com/chimurai/http-proxy-middleware 19 | var proxyTable = config.dev.proxyTable 20 | 21 | var app = express() 22 | var compiler = webpack(webpackConfig) 23 | 24 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 25 | publicPath: webpackConfig.output.publicPath, 26 | quiet: true 27 | }) 28 | 29 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 30 | log: () => {} 31 | }) 32 | // force page reload when html-webpack-plugin template changes 33 | compiler.plugin('compilation', function (compilation) { 34 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 35 | hotMiddleware.publish({ action: 'reload' }) 36 | cb() 37 | }) 38 | }) 39 | 40 | // proxy api requests 41 | Object.keys(proxyTable).forEach(function (context) { 42 | var options = proxyTable[context] 43 | if (typeof options === 'string') { 44 | options = { target: options } 45 | } 46 | app.use(proxyMiddleware(context, options)) 47 | }) 48 | 49 | const serve = (path, cache) => express.static(resolve(path), { 50 | maxAge: cache && isProd ? 60 * 60 * 24 * 30 : 0 51 | }) 52 | app.use('/service-worker.js', serve('../dist/service-worker.js')) 53 | 54 | // handle fallback for HTML5 history API 55 | app.use(require('connect-history-api-fallback')()) 56 | 57 | // serve webpack bundle output 58 | app.use(devMiddleware) 59 | 60 | // enable hot-reload and state-preserving 61 | // compilation error display 62 | app.use(hotMiddleware) 63 | 64 | // serve pure static assets 65 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 66 | app.use(staticPath, express.static('./static')) 67 | 68 | var uri = 'http://localhost:' + port 69 | 70 | devMiddleware.waitUntilValid(function () { 71 | console.log('> Listening at ' + uri + '\n') 72 | }) 73 | 74 | module.exports = app.listen(port, function (err) { 75 | if (err) { 76 | console.log(err) 77 | return 78 | } 79 | 80 | // when env is testing, don't need open it 81 | if (process.env.NODE_ENV !== 'testing') { 82 | opn(uri) 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | // Extract CSS when that option is specified 29 | // (which is the case during production build) 30 | if (options.extract) { 31 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader) 32 | } else { 33 | return ['vue-style-loader', sourceLoader].join('!') 34 | } 35 | } 36 | 37 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 38 | return { 39 | css: generateLoaders(['css']), 40 | postcss: generateLoaders(['css']), 41 | less: generateLoaders(['css', 'less']), 42 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 43 | scss: generateLoaders(['css', 'sass']), 44 | stylus: generateLoaders(['css', 'stylus']), 45 | styl: generateLoaders(['css', 'stylus']) 46 | } 47 | } 48 | 49 | // Generate loaders for standalone style files (outside of .vue) 50 | exports.styleLoaders = function (options) { 51 | var output = [] 52 | var loaders = exports.cssLoaders(options) 53 | for (var extension in loaders) { 54 | var loader = loaders[extension] 55 | output.push({ 56 | test: new RegExp('\\.' + extension + '$'), 57 | loader: loader 58 | }) 59 | } 60 | return output 61 | } 62 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var webpack = require('webpack') 4 | var utils = require('./utils') 5 | var projectRoot = path.resolve(__dirname, '../') 6 | var SWPrecachePlugin = require('sw-precache-webpack-plugin') 7 | 8 | var env = process.env.NODE_ENV 9 | // check env & config/index.js to decide whether to enable CSS source maps for the 10 | // various preprocessor loaders added to vue-loader at the end of this file 11 | var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap) 12 | var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap) 13 | var useCssSourceMap = cssSourceMapDev || cssSourceMapProd 14 | 15 | module.exports = { 16 | entry: { 17 | app: './src/main.js' 18 | }, 19 | output: { 20 | path: config.build.assetsRoot, 21 | publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, 22 | filename: '[name].js' 23 | }, 24 | resolve: { 25 | extensions: ['', '.js', '.vue', '.json'], 26 | fallback: [path.join(__dirname, '../node_modules')], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.common.js', 29 | 'src': path.resolve(__dirname, '../src'), 30 | 'assets': path.resolve(__dirname, '../src/assets'), 31 | 'components': path.resolve(__dirname, '../src/components'), 32 | 'jquery': 'jquery', 33 | } 34 | }, 35 | resolveLoader: { 36 | fallback: [path.join(__dirname, '../node_modules')] 37 | }, 38 | plugins: [ 39 | new SWPrecachePlugin({ 40 | cacheId: 'vue-hn', 41 | filename: 'service-worker.js', 42 | dontCacheBustUrlsMatching: /./, 43 | staticFileGlobsIgnorePatterns: [/index\.html$/, /\.map$/] 44 | }), 45 | new webpack.ProvidePlugin({ 46 | $: "jquery", 47 | jQuery: "jquery" 48 | }), 49 | ], 50 | module: { 51 | preLoaders: [ 52 | { 53 | test: /\.vue$/, 54 | loader: 'eslint', 55 | include: [ 56 | path.join(projectRoot, 'src') 57 | ], 58 | exclude: /node_modules/ 59 | }, 60 | { 61 | test: /\.js$/, 62 | loader: 'eslint', 63 | include: [ 64 | path.join(projectRoot, 'src') 65 | ], 66 | exclude: /node_modules/ 67 | } 68 | ], 69 | loaders: [ 70 | { 71 | test: /\.vue$/, 72 | loader: 'vue' 73 | }, 74 | { 75 | test: /\.js$/, 76 | loader: 'babel', 77 | include: [ 78 | path.join(projectRoot, 'src') 79 | ], 80 | exclude: /node_modules/ 81 | }, 82 | { 83 | test: /\.json$/, 84 | loader: 'json' 85 | }, 86 | { 87 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 88 | loader: 'url', 89 | query: { 90 | limit: 10000, 91 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 92 | } 93 | }, 94 | { 95 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 96 | loader: 'url', 97 | query: { 98 | limit: 10000, 99 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 100 | } 101 | } 102 | ] 103 | }, 104 | eslint: { 105 | formatter: require('eslint-friendly-formatter') 106 | }, 107 | vue: { 108 | loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }), 109 | postcss: [ 110 | require('autoprefixer')({ 111 | browsers: ['last 2 versions'] 112 | }) 113 | ] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var config = require('../config') 2 | var webpack = require('webpack') 3 | var merge = require('webpack-merge') 4 | var utils = require('./utils') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrors = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // eval-source-map is faster for development 19 | devtool: '#eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.optimize.OccurrenceOrderPlugin(), 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoErrorsPlugin(), 28 | // https://github.com/ampedandwired/html-webpack-plugin 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | inject: true 33 | }), 34 | 35 | new FriendlyErrors() 36 | ] 37 | }) 38 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var env = process.env.NODE_ENV === 'testing' 10 | ? require('../config/test.env') 11 | : config.build.env 12 | 13 | var webpackConfig = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }) 16 | }, 17 | devtool: config.build.productionSourceMap ? '#source-map' : false, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 21 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 22 | }, 23 | vue: { 24 | loaders: utils.cssLoaders({ 25 | sourceMap: config.build.productionSourceMap, 26 | extract: true 27 | }) 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | } 38 | }), 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | // extract css into its own file 41 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 42 | // generate dist index.html with correct asset hash for caching. 43 | // you can customize output by editing /index.html 44 | // see https://github.com/ampedandwired/html-webpack-plugin 45 | new HtmlWebpackPlugin({ 46 | filename: process.env.NODE_ENV === 'testing' 47 | ? 'index.html' 48 | : config.build.index, 49 | template: 'index.html', 50 | inject: true, 51 | minify: { 52 | removeComments: true, 53 | collapseWhitespace: true, 54 | removeAttributeQuotes: true 55 | // more options: 56 | // https://github.com/kangax/html-minifier#options-quick-reference 57 | }, 58 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 59 | chunksSortMode: 'dependency' 60 | }), 61 | // split vendor js into its own file 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | name: 'vendor', 64 | minChunks: function (module, count) { 65 | // any required modules inside node_modules are extracted to vendor 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf( 70 | path.join(__dirname, '../node_modules') 71 | ) === 0 72 | ) 73 | } 74 | }), 75 | // extract webpack runtime and module manifest to its own file in order to 76 | // prevent vendor hash from being updated whenever app bundle is updated 77 | new webpack.optimize.CommonsChunkPlugin({ 78 | name: 'manifest', 79 | chunks: ['vendor'] 80 | }) 81 | ] 82 | }) 83 | 84 | if (config.build.productionGzip) { 85 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 86 | 87 | webpackConfig.plugins.push( 88 | new CompressionWebpackPlugin({ 89 | asset: '[path].gz[query]', 90 | algorithm: 'gzip', 91 | test: new RegExp( 92 | '\\.(' + 93 | config.build.productionGzipExtensions.join('|') + 94 | ')$' 95 | ), 96 | threshold: 10240, 97 | minRatio: 0.8 98 | }) 99 | ) 100 | } 101 | 102 | module.exports = webpackConfig 103 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'] 18 | }, 19 | dev: { 20 | env: require('./dev.env'), 21 | port: 8080, 22 | assetsSubDirectory: 'static', 23 | assetsPublicPath: '/', 24 | proxyTable: { 25 | '/api/v3': { 26 | target: 'https://ruby-china.org', 27 | changeOrigin: true, 28 | ws: true, 29 | }, 30 | }, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | http { 2 | 3 | include mime.types; 4 | default_type application/octet-stream; 5 | server { 6 | listen 9000; 7 | server_name ruby-china.local; 8 | root ../ruby-china/dist/; //项目根目录 9 | index index.html; 10 | location ^~ /static/ { 11 | gzip_static on; 12 | expires max; 13 | add_header Cache-Control public; 14 | } 15 | location / { 16 | try_files $uri $uri/ /index.html; 17 | } 18 | location /api/v3{ 19 | proxy_pass https://ruby-china.org/api/v3; 20 | proxy_redirect off; 21 | proxy_buffering off; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ruby China 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ruby-china", 3 | "version": "1.0.0", 4 | "description": "Ruby China Clone", 5 | "author": "hql ", 6 | "private": true, 7 | "homepage": "https://hql123.github.io/vue-ruby-china/", 8 | "scripts": { 9 | "dev": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 12 | "e2e": "node test/e2e/runner.js", 13 | "test": "npm run unit && npm run e2e", 14 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" 15 | }, 16 | "dependencies": { 17 | "jquery": "^3.1.1", 18 | "marked": "^0.3.6", 19 | "moment": "^2.17.1", 20 | "vue": "^2.1.0", 21 | "vue-resource": "^1.1.2", 22 | "vue-router": "^2.2.0", 23 | "vuex": "^2.1.1", 24 | "vuex-router-sync": "^4.1.2" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^6.4.0", 28 | "babel-core": "^6.0.0", 29 | "babel-eslint": "^7.0.0", 30 | "babel-loader": "^6.0.0", 31 | "babel-plugin-istanbul": "^3.0.0", 32 | "babel-plugin-transform-runtime": "^6.0.0", 33 | "babel-preset-es2015": "^6.0.0", 34 | "babel-preset-stage-2": "^6.0.0", 35 | "babel-register": "^6.0.0", 36 | "chai": "^3.5.0", 37 | "chalk": "^1.1.3", 38 | "chromedriver": "^2.21.2", 39 | "compression-webpack-plugin": "^0.3.2", 40 | "connect-history-api-fallback": "^1.1.0", 41 | "cross-env": "^3.1.3", 42 | "cross-spawn": "^4.0.2", 43 | "css-loader": "^0.25.0", 44 | "eslint": "^3.7.1", 45 | "eslint-config-airbnb-base": "^8.0.0", 46 | "eslint-friendly-formatter": "^2.0.5", 47 | "eslint-import-resolver-webpack": "^0.6.0", 48 | "eslint-loader": "^1.5.0", 49 | "eslint-plugin-html": "^1.3.0", 50 | "eslint-plugin-import": "^1.16.0", 51 | "eventsource-polyfill": "^0.9.6", 52 | "express": "^4.13.3", 53 | "extract-text-webpack-plugin": "^1.0.1", 54 | "file-loader": "^0.9.0", 55 | "friendly-errors-webpack-plugin": "^1.1.2", 56 | "function-bind": "^1.0.2", 57 | "html-webpack-plugin": "^2.8.1", 58 | "http-proxy-middleware": "^0.17.2", 59 | "inject-loader": "^2.0.1", 60 | "json-loader": "^0.5.4", 61 | "karma": "^1.3.0", 62 | "karma-coverage": "^1.1.1", 63 | "karma-mocha": "^1.2.0", 64 | "karma-phantomjs-launcher": "^1.0.0", 65 | "karma-sinon-chai": "^1.2.0", 66 | "karma-sourcemap-loader": "^0.3.7", 67 | "karma-spec-reporter": "0.0.26", 68 | "karma-webpack": "^1.7.0", 69 | "lolex": "^1.4.0", 70 | "mocha": "^3.1.0", 71 | "nightwatch": "^0.9.8", 72 | "node-sass": "^4.5.0", 73 | "opn": "^4.0.2", 74 | "ora": "^0.3.0", 75 | "phantomjs-prebuilt": "^2.1.3", 76 | "sass-loader": "^4.1.1", 77 | "selenium-server": "2.53.1", 78 | "semver": "^5.3.0", 79 | "shelljs": "^0.7.4", 80 | "sinon": "^1.17.3", 81 | "sinon-chai": "^2.8.0", 82 | "stylus": "^0.54.5", 83 | "stylus-loader": "^2.4.0", 84 | "sw-precache-webpack-plugin": "^0.8.0", 85 | "url-loader": "^0.5.7", 86 | "vue-loader": "^10.0.0", 87 | "vue-style-loader": "^1.0.0", 88 | "vue-template-compiler": "^2.1.0", 89 | "webpack": "^1.13.2", 90 | "webpack-dev-middleware": "^1.8.3", 91 | "webpack-hot-middleware": "^2.12.2", 92 | "webpack-merge": "^0.14.1" 93 | }, 94 | "engines": { 95 | "node": ">= 4.0.0", 96 | "npm": ">= 3.0.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /public/logo-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hql123/vue-ruby-china/27f6b027c47d7f3946cd4255bebadc7f9d761a78/public/logo-120.png -------------------------------------------------------------------------------- /public/logo-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hql123/vue-ruby-china/27f6b027c47d7f3946cd4255bebadc7f9d761a78/public/logo-144.png -------------------------------------------------------------------------------- /public/logo-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hql123/vue-ruby-china/27f6b027c47d7f3946cd4255bebadc7f9d761a78/public/logo-152.png -------------------------------------------------------------------------------- /public/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hql123/vue-ruby-china/27f6b027c47d7f3946cd4255bebadc7f9d761a78/public/logo-192.png -------------------------------------------------------------------------------- /public/logo-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hql123/vue-ruby-china/27f6b027c47d7f3946cd4255bebadc7f9d761a78/public/logo-384.png -------------------------------------------------------------------------------- /public/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hql123/vue-ruby-china/27f6b027c47d7f3946cd4255bebadc7f9d761a78/public/logo-48.png -------------------------------------------------------------------------------- /src/assets/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Bootstrap v3.3.7 (http://getbootstrap.com) 4 | * Copyright 2011-2016 Twitter, Inc. 5 | * Licensed under the MIT license 6 | */ 7 | /* eslint-disable */ 8 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); 10 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hql123/vue-ruby-china/27f6b027c47d7f3946cd4255bebadc7f9d761a78/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top:65px; 3 | background:#e9eaed; 4 | color:#222527; 5 | } -------------------------------------------------------------------------------- /src/assets/stylesheets/markdown.css: -------------------------------------------------------------------------------- 1 | .markdown .save-state.true { 2 | color: #31708f; 3 | } 4 | .markdown .save-state.false { 5 | color: #a94442; 6 | } 7 | .markdown { 8 | position: relative; 9 | line-height: 1.8em; 10 | font-size: 14px; 11 | text-overflow: ellipsis; 12 | word-wrap: break-word; 13 | font-family: "PingFang SC","Hiragino Sans GB",Helvetica,Arial,"Source Han Sans CN",Roboto,"Heiti SC","Microsoft Yahei",sans-serif !important 14 | } 15 | .markdown .preview { 16 | float: left; 17 | width: 100%; 18 | margin-top: 43px; 19 | border: 1px solid #ccc; 20 | margin-top: 53px; 21 | padding: 0 10px; 22 | } 23 | .markdown img { 24 | max-width: 100% 25 | } 26 | 27 | .markdown p,.markdown pre,.markdown ul,.markdown ol,.markdown blockquote { 28 | margin-bottom: 16px 29 | } 30 | 31 | .markdown p { 32 | font-size: 14px; 33 | line-height: 1.5em 34 | } 35 | 36 | .markdown hr { 37 | border: 2px dashed #F0F4F6; 38 | border-bottom: 0px; 39 | margin: 18px auto; 40 | width: 50% 41 | } 42 | 43 | .markdown blockquote { 44 | margin: 0 18px 15px 18px; 45 | padding: 0; 46 | padding-left: 32px; 47 | border: 0px; 48 | quotes: "“" "”" "‘" "’"; 49 | position: relative; 50 | line-height: 1.45 51 | } 52 | 53 | .markdown blockquote p { 54 | display: inline; 55 | color: #999 56 | } 57 | 58 | .markdown blockquote:before,.markdown blockquote:after { 59 | display: block; 60 | content: "\201C"; 61 | font-size: 35px; 62 | position: absolute; 63 | font-family: serif; 64 | left: 0px; 65 | top: 0px; 66 | color: #aaa 67 | } 68 | 69 | .markdown pre { 70 | font-family: Menlo, Monaco, "Courier New", monospace; 71 | font-size: 12px; 72 | background-color: #f9f9f9; 73 | border: 0px; 74 | border-top: 1px solid #f0f0f0; 75 | border-bottom: 1px solid #f0f0f0; 76 | margin: 0 -15px 15px -15px; 77 | padding: 5px 15px; 78 | color: #444; 79 | overflow: auto; 80 | border-radius: 0px 81 | } 82 | 83 | .markdown pre code { 84 | display: block; 85 | line-height: 150%; 86 | padding: 0 !important; 87 | font-size: 12px !important; 88 | background-color: #f9f9f9 !important; 89 | border: none !important 90 | } 91 | 92 | .markdown p:last-child,.markdown blockquote:last-child,.markdown pre:last-child { 93 | margin-bottom: 0 94 | } 95 | 96 | .markdown pre::-webkit-scrollbar { 97 | height: 8px; 98 | width: 8px 99 | } 100 | 101 | .markdown pre::-webkit-scrollbar-thumb:horizontal { 102 | width: 25px; 103 | background-color: #ccc; 104 | -webkit-border-radius: 4px 105 | } 106 | 107 | .markdown pre::-webkit-scrollbar-track-piece { 108 | margin-bottom: 10px; 109 | background-color: #e5e5e5; 110 | border-bottom-left-radius: 4px 4px; 111 | border-bottom-right-radius: 4px 4px; 112 | border-top-left-radius: 4px 4px; 113 | border-top-right-radius: 4px 4px 114 | } 115 | 116 | .markdown pre::-webkit-scrollbar-thumb:vertical { 117 | height: 25px; 118 | background-color: #ccc; 119 | -webkit-border-radius: 4px; 120 | -webkit-box-shadow: 0 1px 1px white 121 | } 122 | 123 | .markdown code { 124 | display: inline-block; 125 | font-size: 12px !important; 126 | background-color: #f5f5f5 !important; 127 | border: 0px; 128 | color: #444 !important; 129 | padding: 1px 4px !important; 130 | margin: 2px; 131 | border-radius: 3px; 132 | word-break: break-all; 133 | line-height: 20px; 134 | font-family: Monaco,Menlo, "Courier New", monospace 135 | } 136 | 137 | .markdown a:link,.markdown a:visited { 138 | color: #0069D6 !important; 139 | text-decoration: none !important 140 | } 141 | 142 | .markdown a:hover { 143 | text-decoration: underline !important; 144 | color: #00438A !important 145 | } 146 | 147 | .markdown a.mention-floor { 148 | color: #60b566 !important; 149 | margin-right: 3px 150 | } 151 | 152 | .markdown a.mention { 153 | color: #777 !important; 154 | font-weight: bold; 155 | margin-right: 2px 156 | } 157 | 158 | .markdown a.mention b { 159 | color: #777 !important; 160 | font-weight: normal 161 | } 162 | 163 | .markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6 { 164 | font-weight: bold; 165 | text-align: left; 166 | margin-top: 10px !important; 167 | margin-bottom: 16px 168 | } 169 | 170 | .markdown h1 { 171 | font-size: 26px !important; 172 | text-align: center; 173 | margin-bottom: 30px !important 174 | } 175 | 176 | .markdown h2,.markdown h3,.markdown h4 { 177 | text-align: left; 178 | font-weight: bold; 179 | font-size: 16px !important; 180 | line-height: 100%; 181 | margin: 0; 182 | color: #555; 183 | margin-top: 16px; 184 | margin-bottom: 16px; 185 | border-bottom: 1px solid #eee; 186 | padding-bottom: 5px 187 | } 188 | 189 | .markdown h2 { 190 | font-size: 20px !important; 191 | border-bottom-width: 2px; 192 | padding-bottom: 15px; 193 | margin-top: 20px; 194 | margin-bottom: 20px; 195 | color: #111 196 | } 197 | 198 | .markdown h3 { 199 | font-size: 18px !important; 200 | padding-bottom: 10px; 201 | margin-top: 20px; 202 | margin-bottom: 20px; 203 | color: #333 204 | } 205 | 206 | .markdown h5,.markdown h6 { 207 | font-size: 15px; 208 | line-height: 100%; 209 | color: #777 210 | } 211 | 212 | .markdown h6 { 213 | font-size: 14px; 214 | color: #999 215 | } 216 | 217 | .markdown strong { 218 | color: #000 219 | } 220 | 221 | .markdown ul,.markdown ol { 222 | list-style-position: inside; 223 | list-style-type: square; 224 | margin: 0; 225 | margin-bottom: 20px; 226 | padding: 0px 20px 227 | } 228 | 229 | .markdown ul p,.markdown ul blockquote,.markdown ul pre,.markdown ol p,.markdown ol blockquote,.markdown ol pre { 230 | margin-bottom: 8px 231 | } 232 | 233 | .markdown ul li,.markdown ol li { 234 | line-height: 1.6em; 235 | padding: 2px 0; 236 | color: #333 237 | } 238 | 239 | .markdown ul ul,.markdown ol ul { 240 | list-style-type: circle; 241 | margin-bottom: 0px 242 | } 243 | 244 | .markdown ol { 245 | list-style-type: decimal 246 | } 247 | 248 | .markdown ol ol { 249 | list-style-type: lower-alpha; 250 | margin-bottom: 0px 251 | } 252 | 253 | .markdown img { 254 | vertical-align: top; 255 | max-width: 100% 256 | } 257 | 258 | .markdown a.zoom-image { 259 | cursor: zoom-in 260 | } 261 | 262 | .markdown a.at_floor { 263 | color: #60B566 !important 264 | } 265 | 266 | .markdown a.at_user { 267 | color: #0069D6 !important 268 | } 269 | 270 | .markdown img.twemoji { 271 | width: 20px 272 | } 273 | -------------------------------------------------------------------------------- /src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 40 | 45 | 60 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 33 | 57 | 141 | -------------------------------------------------------------------------------- /src/components/Item.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | 114 | -------------------------------------------------------------------------------- /src/components/ItemList.vue: -------------------------------------------------------------------------------- 1 | 18 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/NodeItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 64 | -------------------------------------------------------------------------------- /src/components/NodeList.vue: -------------------------------------------------------------------------------- 1 | 11 | 25 | 26 | 47 | -------------------------------------------------------------------------------- /src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 2 | 13 | 40 | 41 | 51 | -------------------------------------------------------------------------------- /src/components/RepliesList.vue: -------------------------------------------------------------------------------- 1 | 11 | 25 | 36 | -------------------------------------------------------------------------------- /src/components/ReplyItem.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | 116 | -------------------------------------------------------------------------------- /src/components/Sider.vue: -------------------------------------------------------------------------------- 1 | 51 | 111 | -------------------------------------------------------------------------------- /src/components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/TabHeader.vue: -------------------------------------------------------------------------------- 1 | 39 | 65 | 194 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import app from './views/app'; 3 | import './assets/js/bootstrap.min'; 4 | 5 | // actually mount to DOM 6 | app.$mount('#app'); 7 | 8 | if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') { 9 | navigator.serviceWorker.register('/service-worker.js').then(function(registration){ 10 | console.log('Service worker registered : ', registration.scope); 11 | }) 12 | .catch(function(err){ 13 | console.log("Service worker registration failed : ", err); 14 | }); 15 | } 16 | 17 | //array 18 | Array.prototype.group = function(key) { 19 | var result = {}; 20 | this.map(item => ({section: key(item), value: item})).forEach(item => { 21 | result[item.section] = result[item.section] || []; 22 | result[item.section].push(item.value); 23 | }); 24 | return Object.keys(result).map(item => ({section: item, data: result[item]})); 25 | } 26 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | import Vue from 'vue'; 3 | import Router from 'vue-router'; 4 | const Dashboard = resolve => require(['./views/Dashboard'], resolve); 5 | const TopicDetail = resolve => require(['./views/TopicDetail'], resolve); 6 | const Wiki = resolve => require(['./views/Wiki'], resolve); 7 | const Sites = resolve => require(['./views/Sites'], resolve); 8 | import createListView from './views/createListView'; 9 | 10 | Vue.use(Router); 11 | const route = new Router({ 12 | mode: 'history', 13 | scrollBehavior: () => ({ y: 0 }), 14 | routes: [ 15 | { path: '/', component: Dashboard }, 16 | { path: '/topics', component: createListView('default') }, 17 | { path: '/topics/popular', component: createListView('popular') }, 18 | { path: '/topics/no_reply', component: createListView('no_reply') }, 19 | { path: '/topics/last', component: createListView('recent') }, 20 | { path: '/topics/excellent', component: createListView('excellent') }, 21 | { path: '/topics/:id', component: TopicDetail }, 22 | { path: '/homeland', component: createListView('default') }, 23 | { path: '/jobs', component: createListView('default') }, 24 | { path: '/wiki', component: Wiki }, 25 | { path: '/sites', component: Sites }, 26 | ], 27 | }); 28 | 29 | export default route; 30 | -------------------------------------------------------------------------------- /src/store/actions/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Vue from 'vue'; 3 | import VueResource from 'vue-resource'; 4 | 5 | Vue.use(VueResource); 6 | 7 | Vue.http.options.emulateJSON = true; 8 | /* 部署到github pages的配置 9 | Vue.http.options.emulateHTTP = true; 10 | Vue.http.headers.common['Content-Type'] = 'application/json'; 11 | Vue.http.headers.common['Accept'] = 'application/json'; 12 | 13 | // Vue.http.headers.common['Authorization'] = ''; 14 | const context = 'https://ruby-china.org/api/v3'; 15 | */ 16 | const context = '/api/v3'; 17 | const fetchGet = (query) => { 18 | return Vue.http.get(context + query).then((response) => { 19 | return Promise.resolve(response.json()); 20 | }).catch((error) => { 21 | return Promise.reject(error.status); 22 | }); 23 | }; 24 | 25 | function translateOptions(options) { 26 | if (options === undefined) { 27 | return 'offset=0&limit=25'; 28 | } 29 | if (options.indexOf('limit') === -1 && options.indexOf('offset') > -1) { 30 | return `${options}&limit=25`; 31 | } 32 | if (options.indexOf('limit') > -1 && options.indexOf('offset') === -1) { 33 | return `${options}&offset=0`; 34 | } 35 | if (options.indexOf('limit') > -1 && options.indexOf('offset') > -1) { 36 | return options; 37 | } 38 | if (options.indexOf('limit') === -1 && options.indexOf('offset') === -1) { 39 | return `${options}&offset=0&limit=25`; 40 | } 41 | } 42 | 43 | const fetchDefaultTopics = (options = '') => { 44 | return fetchGet(`/topics?${options}`); 45 | }; 46 | const fetchPopularTopics = (options = '') => { 47 | return fetchGet(`/topics?type=popular&${options}`); 48 | }; 49 | const fetchNoReplyTopics = (options = '') => { 50 | return fetchGet(`/topics?type=no_reply&${options}`); 51 | }; 52 | const fetchRecentTopics = (options = '') => { 53 | return fetchGet(`/topics?type=recent&${options}`); 54 | }; 55 | const fetchExcellentTopics = (options = '') => { 56 | return fetchGet(`/topics?type=excellent&${options}`); 57 | }; 58 | 59 | export const fetchTopicsList = (tab, options) => { 60 | const newOptions = translateOptions(options); 61 | switch (tab) { 62 | case 'default': 63 | return fetchDefaultTopics(newOptions); 64 | case 'popular': 65 | return fetchPopularTopics(newOptions); 66 | case 'no_reply': 67 | return fetchNoReplyTopics(newOptions); 68 | case 'recent': 69 | return fetchRecentTopics(newOptions); 70 | case 'excellent': 71 | return fetchExcellentTopics(newOptions); 72 | default: 73 | return fetchDefaultTopics(newOptions); 74 | } 75 | }; 76 | 77 | export const fetchNodesList = () => { 78 | return fetchGet('/nodes'); 79 | }; 80 | 81 | export const fetchTopic = (topic_id) => { 82 | return fetchGet('/topics/' + topic_id); 83 | }; 84 | 85 | export const fetchRepliesList = (topic_id, options) => { 86 | const newOptions = translateOptions(options); 87 | return fetchGet(`/topics/${topic_id}/replies?${newOptions}`) 88 | }; 89 | -------------------------------------------------------------------------------- /src/store/actions/node.js: -------------------------------------------------------------------------------- 1 | import { fetchNodesList } from './api'; 2 | import types from '../mutation-types'; 3 | 4 | export const refreshNodes = ({ commit }) => { 5 | commit(types.REFRESH_NODES); 6 | }; 7 | export const receiveNodes = ({ commit }, { response: json }) => { 8 | commit(types.RECEIVE_NODES_SUCCESS, { 9 | nodes: json.nodes, 10 | }); 11 | }; 12 | export const fetchNodes = ({ commit, dispatch }) => { 13 | commit(types.REQUEST_NODES); 14 | fetchNodesList().then((response) => { 15 | dispatch('receiveNodes', { response }); 16 | }).catch((error) => { 17 | commit(types.RECEIVE_NODES_FAILURE, { error }); 18 | }); 19 | }; 20 | 21 | const shouldFetchNodes = (state) => { 22 | const lists = state.lists; 23 | if (lists.items.length === 0) { 24 | return true; 25 | } 26 | return lists.didInvalidate; 27 | }; 28 | 29 | export const fetchNodesIfNeeded = ({ dispatch, state }) => { 30 | if (shouldFetchNodes(state)) { 31 | dispatch('fetchNodes'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/store/actions/reply.js: -------------------------------------------------------------------------------- 1 | import { fetchRepliesList } from './api'; 2 | import types from '../mutation-types'; 3 | 4 | /* reply list */ 5 | export const receiveReplies = ({ commit }, { response: json }) => { 6 | commit(types.RECEIVE_REPLIES_SUCCESS, { 7 | replies: json.replies, 8 | }); 9 | }; 10 | export const fetchReplies = ({ commit, dispatch }, { topic_id, options }) => { 11 | commit(types.REQUEST_REPLIES); 12 | fetchRepliesList(topic_id, options).then((response) => { 13 | dispatch('receiveReplies', { response }); 14 | }).catch((error) => { 15 | commit(types.RECEIVE_REPLIES_FAILURE, { error }); 16 | }); 17 | }; 18 | 19 | const shouldFetchReplies = (state) => { 20 | const lists = state.lists; 21 | if (lists.items.length === 0) { 22 | return true; 23 | } 24 | if (lists.isFetching) { 25 | return false; 26 | } 27 | return true; 28 | }; 29 | 30 | export const fetchRepliesIfNeeded = ({ dispatch, state }, { topic_id, options }) => { 31 | if (shouldFetchReplies(state)) { 32 | dispatch('fetchReplies', { topic_id, options }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/store/actions/topic.js: -------------------------------------------------------------------------------- 1 | import { fetchTopicsList, fetchTopic } from './api'; 2 | import types from '../mutation-types'; 3 | 4 | /* topic list */ 5 | export const refreshTopics = ({ commit }, { tab }) => { 6 | commit(types.REFRESH_TOPICS, { 7 | tab, 8 | }); 9 | }; 10 | export const receiveTopics = ({ commit }, { tab, response: json }) => { 11 | commit(types.RECEIVE_TOPICS_SUCCESS, { 12 | tab, 13 | topics: json.topics, 14 | }); 15 | }; 16 | export const fetchTopics = ({ commit, dispatch }, { tab, options }) => { 17 | commit(types.REQUEST_TOPICS, { tab }); 18 | fetchTopicsList(tab, options).then((response) => { 19 | dispatch('receiveTopics', { tab, response }); 20 | }).catch((error) => { 21 | commit(types.RECEIVE_TOPICS_FAILURE, { tab, error }); 22 | }); 23 | }; 24 | 25 | const shouldFetchTopics = (state, tab) => { 26 | const lists = state.lists[tab]; 27 | if (!lists) { 28 | return true; 29 | } 30 | if (lists.isFetching) { 31 | return false; 32 | } 33 | return lists.didInvalidate; 34 | }; 35 | 36 | export const fetchTopicsIfNeeded = ({ dispatch, state }, { tab, options }) => { 37 | if (shouldFetchTopics(state, tab)) { 38 | dispatch('fetchTopics', { tab, options }); 39 | } 40 | }; 41 | 42 | /* topic detail */ 43 | export const receiveTopicDetail = ({ commit }, { response: json }) => { 44 | commit(types.RECEIVE_TOPIC_SUCCESS, { 45 | topic: json.topic, 46 | }); 47 | }; 48 | export const fetchTopicDetail = ({ commit, dispatch }, { topic_id }) => { 49 | commit(types.REQUEST_TOPIC); 50 | fetchTopic(topic_id).then((response) => { 51 | dispatch('receiveTopicDetail', { response }); 52 | }).catch((error) => { 53 | commit(types.RECEIVE_TOPIC_FAILURE, { error }); 54 | }); 55 | }; 56 | export const fetchTopicIfNeeded = ({ dispatch, state }, { topic_id }) => { 57 | dispatch('fetchTopicDetail', { topic_id }); 58 | }; 59 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | 2 | const activeType = (state) => { 3 | const current = state.route.path.split('/'); 4 | let newOptions = ''; 5 | const topicId = current[2]; 6 | if (current[1] === 'jobs') { 7 | newOptions += 'node_id=25'; 8 | } 9 | if (current[1] === 'homeland') { 10 | newOptions += 'node_id=23'; 11 | } 12 | if (state.route.query.node_id) { 13 | newOptions += `node_id=${state.route.query.node_id}`; 14 | } 15 | if (state.route.query.page > 1) { 16 | const offset = (state.route.query.page - 1) * 25; 17 | newOptions += `&offset=${offset}`; 18 | } 19 | return { newOptions, topicId }; 20 | }; 21 | export default { activeType }; 22 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import modules from './modules'; 4 | import getters from './getters'; 5 | 6 | Vue.use(Vuex); 7 | 8 | const store = new Vuex.Store({ 9 | modules, 10 | getters, 11 | }); 12 | 13 | if (module.hot) { 14 | module.hot.accept([modules], () => { 15 | const newModules = modules.default; 16 | // 加载新模块 17 | store.hotUpdate({ 18 | modules: newModules, 19 | }); 20 | }); 21 | } 22 | export default store; 23 | -------------------------------------------------------------------------------- /src/store/modules/index.js: -------------------------------------------------------------------------------- 1 | 2 | import topic from './topic'; 3 | import node from './node'; 4 | import reply from './reply'; 5 | 6 | export default { 7 | topic, 8 | node, 9 | reply, 10 | }; 11 | -------------------------------------------------------------------------------- /src/store/modules/node.js: -------------------------------------------------------------------------------- 1 | 2 | import * as nodeActions from '../actions/node'; 3 | import types from '../mutation-types'; 4 | 5 | const initialState = { 6 | isFetching: false, 7 | didInvalidate: false, 8 | items: [], 9 | error: '', 10 | }; 11 | const state = { 12 | lists: initialState, 13 | }; 14 | 15 | // getters 16 | const getters = { 17 | groupNodes(store) { 18 | let nodes = store.lists.items; 19 | nodes = nodes.length > 0 ? nodes.group(item => item.section_name) : []; 20 | return nodes; 21 | }, 22 | }; 23 | 24 | // actions 25 | const actions = nodeActions; 26 | /* eslint-disable */ 27 | const mutations = { 28 | [types.REFRESH_NODES] (state) { 29 | state.lists = { 30 | ...state.lists, 31 | didInvalidate: true, 32 | }; 33 | }, 34 | [types.REQUEST_NODES] (state) { 35 | state.lists = { 36 | ...state.lists, 37 | isFetching: true, 38 | didInvalidate: false, 39 | }; 40 | }, 41 | [types.RECEIVE_NODES_SUCCESS] (state, { nodes }) { 42 | state.lists = { 43 | ...state.lists, 44 | isFetching: false, 45 | didInvalidate: false, 46 | items: nodes, 47 | }; 48 | }, 49 | [types.RECEIVE_NODES_FAILURE] (state, { tab, error }) { 50 | state.lists = { 51 | ...state.lists, 52 | isFetching: false, 53 | didInvalidate: false, 54 | error: error, 55 | }; 56 | }, 57 | }; 58 | /* eslint-disable */ 59 | 60 | export default { 61 | state, 62 | mutations, 63 | getters, 64 | actions, 65 | }; 66 | -------------------------------------------------------------------------------- /src/store/modules/reply.js: -------------------------------------------------------------------------------- 1 | 2 | import * as replyActions from '../actions/reply'; 3 | import types from '../mutation-types'; 4 | 5 | const initialState = { 6 | isFetching: false, 7 | didInvalidate: false, 8 | items: [], 9 | error: '', 10 | }; 11 | const state = { 12 | lists: initialState, 13 | }; 14 | 15 | // getters 16 | const getters = {}; 17 | 18 | // actions 19 | const actions = replyActions; 20 | /* eslint-disable */ 21 | const mutations = { 22 | /* topic list */ 23 | [types.REQUEST_REPLIES] (state) { 24 | state.lists = { 25 | ...state.lists, 26 | isFetching: true, 27 | }; 28 | }, 29 | [types.RECEIVE_REPLIES_SUCCESS] (state, { replies }) { 30 | state.lists = { 31 | ...state.lists, 32 | isFetching: false, 33 | items: replies 34 | }; 35 | }, 36 | [types.RECEIVE_REPLIES_FAILURE] (state, { error }) { 37 | state.lists = { 38 | ...state.lists, 39 | isFetching: false, 40 | error: error 41 | }; 42 | }, 43 | }; 44 | /* eslint-disable */ 45 | 46 | export default { 47 | state, 48 | mutations, 49 | getters, 50 | actions, 51 | }; 52 | -------------------------------------------------------------------------------- /src/store/modules/topic.js: -------------------------------------------------------------------------------- 1 | 2 | import * as topicActions from '../actions/topic'; 3 | import types from '../mutation-types'; 4 | 5 | const initialState = { 6 | isFetching: false, 7 | didInvalidate: false, 8 | items: [], 9 | error: '', 10 | }; 11 | const state = { 12 | lists: { 13 | default: initialState, 14 | popular: initialState, 15 | recent: initialState, 16 | no_reply: initialState, 17 | excellent: initialState, 18 | }, 19 | detail: { 20 | isFetching: false, 21 | error: '', 22 | item: {}, 23 | }, 24 | }; 25 | 26 | // getters 27 | const getters = {}; 28 | 29 | // actions 30 | const actions = topicActions; 31 | /* eslint-disable */ 32 | const mutations = { 33 | /* topic list */ 34 | [types.REFRESH_TOPICS] (state, { tab }) { 35 | state.lists[tab] = { 36 | ...state.lists[tab], 37 | didInvalidate: true, 38 | }; 39 | }, 40 | [types.REQUEST_TOPICS] (state, { tab }) { 41 | state.lists[tab].isFetching = true; 42 | state.lists[tab].didInvalidate = false; 43 | }, 44 | [types.RECEIVE_TOPICS_SUCCESS] (state, { tab, topics }) { 45 | state.lists[tab].isFetching = false; 46 | state.lists[tab].didInvalidate = false; 47 | state.lists[tab].items = topics; 48 | }, 49 | [types.RECEIVE_TOPICS_FAILURE] (state, { tab, error }) { 50 | state.lists[tab].error = error; 51 | state.lists[tab].didInvalidate = false; 52 | state.lists[tab].isFetching = false; 53 | }, 54 | /* topic detail */ 55 | [types.REQUEST_TOPIC] (state) { 56 | state.detail = { 57 | ...state.detail, 58 | isFetching: true, 59 | } 60 | }, 61 | [types.RECEIVE_TOPIC_SUCCESS] (state, { topic }) { 62 | state.detail = { 63 | ...state.detail, 64 | isFetching: false, 65 | item: topic, 66 | } 67 | }, 68 | [types.RECEIVE_TOPIC_FAILURE] (state, { error }) { 69 | state.detail = { 70 | ...state.detail, 71 | isFetching: false, 72 | error: error, 73 | } 74 | }, 75 | }; 76 | /* eslint-disable */ 77 | 78 | export default { 79 | state, 80 | mutations, 81 | getters, 82 | actions, 83 | }; 84 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | const types = { 2 | SWITCH_TYPE: 'SWITCH_TYPE', 3 | REFRESH_TOPICS: 'REFRESH_TOPICS', 4 | REQUEST_TOPICS: 'REQUEST_TOPICS', 5 | RECEIVE_TOPICS_SUCCESS: 'RECEIVE_TOPICS_SUCCESS', 6 | RECEIVE_TOPICS_FAILURE: 'RECEIVE_TOPICS_FAILURE', 7 | REQUEST_TOPIC: 'REQUEST_TOPIC', 8 | RECEIVE_TOPIC_SUCCESS: 'RECEIVE_TOPIC_SUCCESS', 9 | RECEIVE_TOPIC_FAILURE: 'RECEIVE_TOPIC_FAILURE', 10 | REFRESH_NODES: 'REFRESH_NODES', 11 | REQUEST_NODES: 'REQUEST_NODES', 12 | RECEIVE_NODES_SUCCESS: 'RECEIVE_NODES_SUCCESS', 13 | RECEIVE_NODES_FAILURE: 'RECEIVE_NODES_FAILURE', 14 | REQUEST_REPLIES: 'REQUEST_REPLIES', 15 | RECEIVE_REPLIES_SUCCESS: 'RECEIVE_REPLIES_SUCCESS', 16 | RECEIVE_REPLIES_FAILURE: 'RECEIVE_REPLIES_FAILURE', 17 | }; 18 | export default types; 19 | -------------------------------------------------------------------------------- /src/views/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 46 | 50 | -------------------------------------------------------------------------------- /src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 92 | 138 | 264 | -------------------------------------------------------------------------------- /src/views/Sites.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/TopicDetail.vue: -------------------------------------------------------------------------------- 1 | 42 | 89 | 154 | -------------------------------------------------------------------------------- /src/views/Wiki.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { sync } from 'vuex-router-sync'; 3 | import App from './App.vue'; 4 | import store from '../store'; 5 | import router from '../router'; 6 | 7 | 8 | // sync the router with the vuex store. 9 | // this registers `store.state.route` 10 | sync(store, router); 11 | 12 | // create the app instance. 13 | // here we inject the router and store to all child components, 14 | // making them available everywhere as `this.$router` and `this.$store`. 15 | const app = new Vue({ 16 | router, 17 | store, 18 | ...App, 19 | }); 20 | 21 | // expose the app, the router and the store. 22 | // note we are not mounting the app here, since bootstrapping will be 23 | // different depending on whether we are in a browser or on the server. 24 | export default app; 25 | -------------------------------------------------------------------------------- /src/views/createListView.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | import { mapState, mapGetters } from 'vuex'; 3 | import $ from 'jquery'; 4 | import ItemList from '../components/ItemList'; 5 | import Sider from '../components/Sider'; 6 | import TabHeader from '../components/TabHeader'; 7 | 8 | const createListView = tab => ({ 9 | name: `${tab}-topics-view`, 10 | components: { 11 | ItemList, 12 | Sider, 13 | }, 14 | 15 | computed: { 16 | node() { 17 | const { newOptions } = this.$store.getters.activeType; 18 | const nodeid = newOptions.indexOf('node_id') > -1 ? newOptions.split('node_id=')[1].split('&')[0] : 0; 19 | return nodeid > 0 ? this.nodes.find(item => item.id === Number(nodeid)) : {}; 20 | }, 21 | nodesGroup() { 22 | return this.$store.getters.groupNodes; 23 | }, 24 | ...mapState({ 25 | current: state => state.route.path.split('/'), 26 | loading: state => state.topic.lists[tab].isFetching, 27 | items: state => state.topic.lists[tab].items, 28 | nodes: state => state.node.lists.items, 29 | options: state => state.route.fullPath.split('?')[1], 30 | }), 31 | ...mapGetters([ 32 | 'activeType', 33 | 'groupNodes', 34 | ]), 35 | }, 36 | watch: { 37 | options(to, from){ 38 | this.fetchNodes(); 39 | this.fetchTopics(); 40 | this.hideModal(); 41 | }, 42 | }, 43 | render(createElement) { 44 | return createElement('div', {}, [ 45 | createElement(TabHeader, { props: { 46 | current: this.current, 47 | options: this.activeType.newOptions, 48 | node: this.node, 49 | nodes: this.nodesGroup, 50 | } }), 51 | createElement('div', { attrs: { class: 'container' } }, [ 52 | createElement('div', { attrs: { class: 'row' } }, [ 53 | createElement('div', { attrs: { class: 'col-md-9' } }, [ 54 | createElement( 55 | ItemList, 56 | { props: { 57 | loading: this.loading, 58 | items: this.items, 59 | } } 60 | ), 61 | ]), 62 | createElement('div', { attrs: { class: 'col-md-3' } }, [ 63 | createElement(Sider, { props: { isLogining: false } }), 64 | ]), 65 | ]), 66 | ]), 67 | ]); 68 | }, 69 | beforeMount() { 70 | this.fetchNodes(); 71 | this.fetchTopics(); 72 | this.hideModal(); 73 | }, 74 | methods: { 75 | fetchNodes() { 76 | this.$store.dispatch('fetchNodesIfNeeded'); 77 | }, 78 | fetchTopics() { 79 | const { newOptions } = this.activeType; 80 | this.$store.dispatch('refreshTopics', { tab }); 81 | this.$store.dispatch('fetchTopicsIfNeeded', { tab, options: newOptions }); 82 | }, 83 | hideModal() { 84 | $('#nodeModal').modal('hide'); 85 | $('body').removeClass('modal-open'); 86 | $('.modal-backdrop').remove(); 87 | }, 88 | }, 89 | }); 90 | 91 | export default createListView; 92 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hql123/vue-ruby-china/27f6b027c47d7f3946cd4255bebadc7f9d761a78/static/.gitkeep -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count; 11 | this.expected = count; 12 | this.pass = function (val) { 13 | return val === this.expected; 14 | } 15 | this.value = function (res) { 16 | return res.value; 17 | } 18 | this.command = function (cb) { 19 | var self = this; 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length; 22 | }, [selector], function (res) { 23 | cb.call(self, res); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/guide#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar', 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing'; 3 | var server = require('../../build/dev-server.js'); 4 | 5 | // 2. run the nightwatch test suite against it 6 | // to run in additional browsers: 7 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 8 | // 2. add it to the --env flag below 9 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 10 | // For more information on Nightwatch's config file, see 11 | // http://nightwatchjs.org/guide#settings-file 12 | var opts = process.argv.slice(2); 13 | if (opts.indexOf('--config') === -1) { 14 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']); 15 | } 16 | if (opts.indexOf('--env') === -1) { 17 | opts = opts.concat(['--env', 'chrome']); 18 | } 19 | 20 | var spawn = require('cross-spawn'); 21 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }); 22 | 23 | runner.on('exit', function (code) { 24 | server.close(); 25 | process.exit(code); 26 | }); 27 | 28 | runner.on('error', function (err) { 29 | server.close(); 30 | throw err; 31 | }); 32 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function test(browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL; 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end(); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | /* eslint-disable no-extend-native */ 3 | Function.prototype.bind = require('function-bind'); 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/); 7 | testsContext.keys().forEach(testsContext); 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('src', true, /^\.\/(?!main(\.js)?$)/); 13 | srcContext.keys().forEach(srcContext); 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var path = require('path'); 7 | var merge = require('webpack-merge'); 8 | var baseConfig = require('../../build/webpack.base.conf'); 9 | var utils = require('../../build/utils'); 10 | var webpack = require('webpack'); 11 | var projectRoot = path.resolve(__dirname, '../../'); 12 | 13 | var webpackConfig = merge(baseConfig, { 14 | // use inline sourcemap for karma-sourcemap-loader 15 | module: { 16 | loaders: utils.styleLoaders() 17 | }, 18 | devtool: '#inline-source-map', 19 | vue: { 20 | loaders: { 21 | js: 'babel-loader' 22 | } 23 | }, 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env': require('../../config/test.env') 27 | }) 28 | ] 29 | }); 30 | 31 | // no need for app entry during tests 32 | delete webpackConfig.entry; 33 | 34 | // Use babel for test files too 35 | webpackConfig.module.loaders.some(function (loader, i) { 36 | if (/^babel(-loader)?$/.test(loader.loader)) { 37 | loader.include.push(path.resolve(projectRoot, 'test/unit')); 38 | return true; 39 | } 40 | }); 41 | 42 | module.exports = function (config) { 43 | config.set({ 44 | // to run in additional browsers: 45 | // 1. install corresponding karma launcher 46 | // http://karma-runner.github.io/0.13/config/browsers.html 47 | // 2. add it to the `browsers` array below. 48 | browsers: ['PhantomJS'], 49 | frameworks: ['mocha', 'sinon-chai'], 50 | reporters: ['spec', 'coverage'], 51 | files: ['./index.js'], 52 | preprocessors: { 53 | './index.js': ['webpack', 'sourcemap'] 54 | }, 55 | webpack: webpackConfig, 56 | webpackMiddleware: { 57 | noInfo: true, 58 | }, 59 | coverageReporter: { 60 | dir: './coverage', 61 | reporters: [ 62 | { type: 'lcov', subdir: '.' }, 63 | { type: 'text-summary' }, 64 | ] 65 | }, 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Hello from 'src/components/Hello'; 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const vm = new Vue({ 7 | el: document.createElement('div'), 8 | render: (h) => h(Hello), 9 | }); 10 | expect(vm.$el.querySelector('.hello h1').textContent) 11 | .to.equal('Welcome to Your Vue.js App'); 12 | }); 13 | }); 14 | --------------------------------------------------------------------------------