├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── build ├── build.js ├── check-versions.js ├── logo.png ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── package.json ├── src ├── App.vue ├── apis │ ├── EssayEditorApis.js │ ├── FeedBackApis.js │ ├── MindTableEditorApis.js │ ├── RoadmapEditorApis.js │ ├── User.js │ ├── UserProfileApis.js │ └── util.js ├── assets │ ├── HomePageLogo.png │ ├── LoginLogo.png │ ├── RoadmapDefault.png │ ├── css │ │ └── emoji.css │ ├── favicon.ico │ ├── img │ │ ├── emoji_sprite.png │ │ ├── face_logo.png │ │ └── heading.jpg │ ├── login │ │ ├── input_password.png │ │ └── input_user.png │ ├── logo.png │ └── welcome │ │ ├── books.png │ │ ├── folder.png │ │ ├── mindmap.png │ │ └── recommand.png ├── components │ ├── AddCommentForm.vue │ ├── AddConnectionForm.vue │ ├── AddNodeForm.vue │ ├── ArticleStatisExpand.vue │ ├── ArticleStatistics.vue │ ├── Breadcrumb.js │ ├── DelCommentForm.vue │ ├── DelConnectionForm.vue │ ├── DelNodeForm.vue │ ├── EditRoadmapDescriptionForm.vue │ ├── ErrPush.js │ ├── EssayTable.vue │ ├── EssayTableExpand.vue │ ├── FileItem.vue │ ├── HelloWorld.vue │ ├── Likes.vue │ ├── LoadRoadmapForm.vue │ ├── MindTable.vue │ ├── MindTableExpand.vue │ ├── ModifyAliasForm.vue │ ├── ModifyColor.vue │ ├── ModifyCommentForm.vue │ ├── ModifyNodeForm.vue │ ├── NoteMarkdown.vue │ ├── PaperRecommend.vue │ ├── Request.vue │ ├── RoadItemEditor.vue │ ├── RoadmapCardTable.vue │ ├── RoadmapTable.vue │ ├── RoadmapWindow.vue │ ├── TableItemEditor.vue │ ├── UserReportButton.vue │ ├── comment │ │ ├── Comment.vue │ │ └── children │ │ │ ├── Emoji.vue │ │ │ └── EmojiPanel.vue │ └── roadmap │ │ ├── Roadmap.vue │ │ ├── parser │ │ ├── emojis.js │ │ └── regex.js │ │ ├── style.sass │ │ └── utils │ │ ├── d3.js │ │ ├── dimensions.js │ │ ├── nodeToHTML.js │ │ └── subnodesToHTML.js ├── main.js ├── router │ └── index.js ├── views │ ├── ArticleTableView.vue │ ├── EssayEditor.vue │ ├── EssayReaderView.vue │ ├── EssayRoadmapBindReader.vue │ ├── EssayTableView.vue │ ├── MarkDownEditorView.vue │ ├── RoadmapEditorView.vue │ ├── RoadmapLayout.vue │ ├── RoadmapReaderView.vue │ ├── RoadmapTableView.vue │ ├── UserLoginView.vue │ ├── UserProfileView.vue │ ├── UserRegisterView.vue │ └── WelcomeCardView.vue └── vuex │ └── index.js ├── static └── .gitkeep ├── test └── unit │ ├── .eslintrc │ ├── jest.conf.js │ ├── setup.js │ └── specs │ └── HelloWorld.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.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/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 12 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 13 | extends: ['plugin:vue/essential', 'airbnb-base'], 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'vue' 17 | ], 18 | // check if imports actually resolve 19 | settings: { 20 | 'import/resolver': { 21 | webpack: { 22 | config: 'build/webpack.base.conf.js' 23 | } 24 | } 25 | }, 26 | // add your custom rules here 27 | rules: { 28 | // don't require .vue extension when importing 29 | 'import/extensions': ['error', 'always', { 30 | js: 'never', 31 | vue: 'never' 32 | }], 33 | // disallow reassignment of function parameters 34 | // disallow parameter object manipulation except for specific exclusions 35 | 'no-param-reassign': ['error', { 36 | props: true, 37 | ignorePropertyModificationsFor: [ 38 | 'state', // for vuex state 39 | 'acc', // for reduce accumulators 40 | 'e' // for e.returnvalue 41 | ] 42 | }], 43 | // allow optionalDependencies 44 | 'import/no-extraneous-dependencies': ['error', { 45 | optionalDependencies: ['test/unit/index.js'] 46 | }], 47 | // allow debugger during development 48 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 49 | "vue/no-parsing-error": [2, { "x-invalid-end-tag": false }], 50 | "linebreak-style": [0 ,"error", "windows"], 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | package-lock.json 4 | /dist/ 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | /test/unit/coverage/ 9 | 10 | # sass cache 11 | *.sassc 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roadmap-frontend 2 | 3 | > A Vue.js project 4 | 5 | ## Live Demo 6 | 7 | Please visit Knowledge-RoadMap: http://47.94.141.56 8 | 9 | Google Chrome Browser is recommended. 10 | 11 | ## Help Doc 12 | 13 | Please **sign up / login** and **read** [help document](http://47.94.141.56/essayReader?sharedId=a1cfabe9f960cc061868020f210575455fb87abaa710cabbdad1379870d11e70/) in our Live Demo Website 14 | 15 | Or read [this blog](https://www.cnblogs.com/minjiekaifa/p/13080164.html) 16 | 17 | ## Backend Repository 18 | 19 | Backend is built with django. 20 | 21 | https://github.com/MinJieDev/Roadmap-Backend 22 | 23 | ## Build Setup 24 | 25 | ``` bash 26 | # install dependencies 27 | yarn install 28 | 29 | # serve with hot reload at localhost:8080 30 | yarn run dev 31 | 32 | # build for production with minification 33 | yarn run build 34 | ``` 35 | 36 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 37 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/build/logo.png -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | ...(config.dev.useEslint ? [createLintingRule()] : []), 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueLoaderConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 56 | loader: 'url-loader', 57 | options: { 58 | limit: 10000, 59 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 60 | } 61 | }, 62 | { 63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 64 | loader: 'url-loader', 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | node: { 81 | // prevent webpack from injecting useless setImmediate polyfill because Vue 82 | // source contains it (although only uses it if it's native). 83 | setImmediate: false, 84 | // prevent webpack from injecting mocks to Node native modules 85 | // that does not make sense for the client 86 | dgram: 'empty', 87 | fs: 'empty', 88 | net: 'empty', 89 | tls: 'empty', 90 | child_process: 'empty' 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | favicon: 'src/assets/favicon.ico', 59 | inject: true 60 | }), 61 | // copy custom static assets 62 | new CopyWebpackPlugin([ 63 | { 64 | from: path.resolve(__dirname, '../static'), 65 | to: config.dev.assetsSubDirectory, 66 | ignore: ['.*'] 67 | } 68 | ]) 69 | ] 70 | }) 71 | 72 | module.exports = new Promise((resolve, reject) => { 73 | portfinder.basePort = process.env.PORT || config.dev.port 74 | portfinder.getPort((err, port) => { 75 | if (err) { 76 | reject(err) 77 | } else { 78 | // publish the new Port, necessary for e2e tests 79 | process.env.PORT = port 80 | // add port to devServer config 81 | devWebpackConfig.devServer.port = port 82 | 83 | // Add FriendlyErrorsPlugin 84 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 85 | compilationSuccessInfo: { 86 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 87 | }, 88 | onErrors: config.dev.notifyOnErrors 89 | ? utils.createNotifierCallback() 90 | : undefined 91 | })) 92 | 93 | resolve(devWebpackConfig) 94 | } 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = process.env.NODE_ENV === 'testing' 15 | ? require('../config/test.env') 16 | : require('../config/prod.env') 17 | 18 | const webpackConfig = merge(baseWebpackConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ 21 | sourceMap: config.build.productionSourceMap, 22 | extract: true, 23 | usePostCSS: true 24 | }) 25 | }, 26 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 31 | }, 32 | plugins: [ 33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 34 | new webpack.DefinePlugin({ 35 | 'process.env': env 36 | }), 37 | new UglifyJsPlugin({ 38 | uglifyOptions: { 39 | compress: { 40 | warnings: false 41 | } 42 | }, 43 | sourceMap: config.build.productionSourceMap, 44 | parallel: true 45 | }), 46 | // extract css into its own file 47 | new ExtractTextPlugin({ 48 | filename: utils.assetsPath('css/[name].[contenthash].css'), 49 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 53 | allChunks: true, 54 | }), 55 | // Compress extracted CSS. We are using this plugin so that possible 56 | // duplicated CSS from different components can be deduped. 57 | new OptimizeCSSPlugin({ 58 | cssProcessorOptions: config.build.productionSourceMap 59 | ? { safe: true, map: { inline: false } } 60 | : { safe: true } 61 | }), 62 | // generate dist index.html with correct asset hash for caching. 63 | // you can customize output by editing /index.html 64 | // see https://github.com/ampedandwired/html-webpack-plugin 65 | new HtmlWebpackPlugin({ 66 | filename: process.env.NODE_ENV === 'testing' 67 | ? 'index.html' 68 | : config.build.index, 69 | template: 'index.html', 70 | favicon: 'src/assets/favicon.ico', 71 | inject: true, 72 | minify: { 73 | removeComments: true, 74 | collapseWhitespace: true, 75 | removeAttributeQuotes: true 76 | // more options: 77 | // https://github.com/kangax/html-minifier#options-quick-reference 78 | }, 79 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 80 | chunksSortMode: 'dependency' 81 | }), 82 | // keep module.id stable when vendor modules does not change 83 | new webpack.HashedModuleIdsPlugin(), 84 | // enable scope hoisting 85 | new webpack.optimize.ModuleConcatenationPlugin(), 86 | // split vendor js into its own file 87 | new webpack.optimize.CommonsChunkPlugin({ 88 | name: 'vendor', 89 | minChunks (module) { 90 | // any required modules inside node_modules are extracted to vendor 91 | return ( 92 | module.resource && 93 | /\.js$/.test(module.resource) && 94 | module.resource.indexOf( 95 | path.join(__dirname, '../node_modules') 96 | ) === 0 97 | ) 98 | } 99 | }), 100 | // extract webpack runtime and module manifest to its own file in order to 101 | // prevent vendor hash from being updated whenever app bundle is updated 102 | new webpack.optimize.CommonsChunkPlugin({ 103 | name: 'manifest', 104 | minChunks: Infinity 105 | }), 106 | // This instance extracts shared chunks from code splitted chunks and bundles them 107 | // in a separate chunk, similar to the vendor chunk 108 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 109 | new webpack.optimize.CommonsChunkPlugin({ 110 | name: 'app', 111 | async: 'vendor-async', 112 | children: true, 113 | minChunks: 3 114 | }), 115 | 116 | // copy custom static assets 117 | new CopyWebpackPlugin([ 118 | { 119 | from: path.resolve(__dirname, '../static'), 120 | to: config.build.assetsSubDirectory, 121 | ignore: ['.*'] 122 | } 123 | ]) 124 | ] 125 | }) 126 | 127 | if (config.build.productionGzip) { 128 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 129 | 130 | webpackConfig.plugins.push( 131 | new CompressionWebpackPlugin({ 132 | asset: '[path].gz[query]', 133 | algorithm: 'gzip', 134 | test: new RegExp( 135 | '\\.(' + 136 | config.build.productionGzipExtensions.join('|') + 137 | ')$' 138 | ), 139 | threshold: 10240, 140 | minRatio: 0.8 141 | }) 142 | ) 143 | } 144 | 145 | if (config.build.bundleAnalyzerReport) { 146 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 147 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 148 | } 149 | 150 | module.exports = webpackConfig 151 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path'); 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: { 14 | '/api': { 15 | target: 'http://47.94.141.56:8000/', 16 | changeOrigin: true, 17 | pathRewrite: { 18 | '^/api': '/api' 19 | } 20 | }, 21 | }, 22 | 23 | // Various Dev Server settings 24 | host: 'localhost', // can be overwritten by process.env.HOST 25 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 26 | autoOpenBrowser: false, 27 | errorOverlay: true, 28 | notifyOnErrors: true, 29 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 30 | 31 | // Use Eslint Loader? 32 | // If true, your code will be linted during bundling and 33 | // linting errors and warnings will be shown in the console. 34 | useEslint: true, 35 | // If true, eslint errors and warnings will also be shown in the error overlay 36 | // in the browser. 37 | showEslintErrorsInOverlay: false, 38 | 39 | /** 40 | * Source Maps 41 | */ 42 | 43 | // https://webpack.js.org/configuration/devtool/#development 44 | devtool: 'cheap-module-eval-source-map', 45 | 46 | // If you have problems debugging vue-files in devtools, 47 | // set this to false - it *may* help 48 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 49 | cacheBusting: true, 50 | 51 | cssSourceMap: true 52 | }, 53 | 54 | build: { 55 | // Template for index.html 56 | index: path.resolve(__dirname, '../dist/index.html'), 57 | 58 | // Paths 59 | assetsRoot: path.resolve(__dirname, '../dist'), 60 | assetsSubDirectory: 'static', 61 | assetsPublicPath: '/', 62 | 63 | /** 64 | * Source Maps 65 | */ 66 | 67 | productionSourceMap: true, 68 | // https://webpack.js.org/configuration/devtool/#production 69 | devtool: '#source-map', 70 | 71 | // Gzip off by default as many popular static hosts such as 72 | // Surge or Netlify already gzip all static assets for you. 73 | // Before setting to `true`, make sure to: 74 | // npm install --save-dev compression-webpack-plugin 75 | productionGzip: false, 76 | productionGzipExtensions: ['js', 'css'], 77 | 78 | // Run the build command with an extra argument to 79 | // View the bundle analyzer report after build finishes: 80 | // `npm run build --report` 81 | // Set to `true` or `false` to always turn it on or off 82 | bundleAnalyzerReport: process.env.npm_config_report 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 知识路书 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roadmap-frontend", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "MinJieDev", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "unit": "jest --config test/unit/jest.conf.js --coverage", 11 | "test": "npm run unit", 12 | "lint": "eslint --ext .js,.vue src test/unit", 13 | "build": "node build/build.js" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.19.2", 17 | "bibtex-parse-js": "^0.0.24", 18 | "d3": "^5.16.0", 19 | "dom-to-image": "^2.6.0", 20 | "github-markdown-css": "^4.0.0", 21 | "global": "^4.4.0", 22 | "json-loader": "^0.5.7", 23 | "less-loader": "4.1.0", 24 | "lodash": "^4.17.15", 25 | "prism": "^4.1.2", 26 | "schart.js": "^3.0.0", 27 | "simplemde": "^1.11.2", 28 | "view-design": "^4.2.0", 29 | "vue": "^2.5.2", 30 | "vue-clipboard2": "^0.3.1", 31 | "vue-cookies": "^1.7.0", 32 | "vue-markdown": "^2.2.4", 33 | "vue-mindmap": "^0.0.4", 34 | "vue-router": "^3.0.1", 35 | "vue-schart": "^2.0.0", 36 | "vuedraggable": "^2.23.2", 37 | "vuex": "^3.1.3" 38 | }, 39 | "devDependencies": { 40 | "autoprefixer": "^7.1.2", 41 | "babel-core": "^6.22.1", 42 | "babel-eslint": "^8.2.1", 43 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 44 | "babel-jest": "^21.0.2", 45 | "babel-loader": "^7.1.1", 46 | "babel-plugin-dynamic-import-node": "^1.2.0", 47 | "babel-plugin-syntax-jsx": "^6.18.0", 48 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 49 | "babel-plugin-transform-runtime": "^6.22.0", 50 | "babel-plugin-transform-vue-jsx": "^3.5.0", 51 | "babel-preset-env": "^1.3.2", 52 | "babel-preset-stage-2": "^6.22.0", 53 | "chalk": "^2.0.1", 54 | "copy-webpack-plugin": "^4.0.1", 55 | "css-loader": "^0.28.0", 56 | "eslint": "^4.15.0", 57 | "eslint-config-airbnb-base": "^11.3.0", 58 | "eslint-friendly-formatter": "^3.0.0", 59 | "eslint-import-resolver-webpack": "^0.8.3", 60 | "eslint-loader": "^1.7.1", 61 | "eslint-plugin-import": "^2.7.0", 62 | "eslint-plugin-vue": "^4.0.0", 63 | "extract-text-webpack-plugin": "^3.0.0", 64 | "file-loader": "^1.1.4", 65 | "friendly-errors-webpack-plugin": "^1.6.1", 66 | "html-webpack-plugin": "^2.30.1", 67 | "jest": "^22.0.4", 68 | "jest-serializer-vue": "^0.3.0", 69 | "node-notifier": "^5.1.2", 70 | "optimize-css-assets-webpack-plugin": "^3.2.0", 71 | "ora": "^1.2.0", 72 | "portfinder": "^1.0.13", 73 | "postcss-import": "^11.0.0", 74 | "postcss-loader": "^2.0.8", 75 | "postcss-url": "^7.2.1", 76 | "rimraf": "^2.6.0", 77 | "semver": "^5.3.0", 78 | "shelljs": "^0.7.6", 79 | "uglifyjs-webpack-plugin": "^1.1.1", 80 | "url-loader": "^0.5.8", 81 | "vue-jest": "^1.0.2", 82 | "vue-loader": "^13.3.0", 83 | "vue-style-loader": "^4.1.2", 84 | "vue-template-compiler": "^2.5.2", 85 | "webpack": "^3.6.0", 86 | "webpack-bundle-analyzer": "^2.9.0", 87 | "webpack-dev-server": "^2.9.1", 88 | "webpack-merge": "^4.1.0" 89 | }, 90 | "engines": { 91 | "node": ">= 6.0.0", 92 | "npm": ">= 3.0.0" 93 | }, 94 | "browserslist": [ 95 | "> 1%", 96 | "last 2 versions", 97 | "not ie <= 8" 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /src/apis/EssayEditorApis.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { req, reqSingle } from '../apis/util'; 3 | 4 | // export const createMDnote = (title, note) => req('/api/essays/', 5 | // 'POST', {}, { title, text: note }); 6 | function splashEnd(id) { 7 | let idSplash = id; 8 | if (!_(id).endsWith('/')) { 9 | idSplash = `${id}/`; 10 | } 11 | return idSplash; 12 | } 13 | 14 | export const deleteEssay = id => reqSingle(`api/essays/${id}.json`, 'DELETE'); 15 | 16 | export const getEssay = id => reqSingle(`/api/essays/${id}.json`, 'GET'); 17 | 18 | export const changeEssay = data => reqSingle(`/api/essays/${data.id}.json`, 'PATCH', data); 19 | 20 | export const updateEssay = (id, title, text) => reqSingle(`/api/essays/${id}.json`, 'PATCH', { title, text }); 21 | 22 | export const postEssayShareLink = id => req('/api/share/essay/', 'POST', {}, { id }); 23 | 24 | export const getEssayShareLink = id => req(`/api/share/essay/${splashEnd(id)}`, 'GET'); 25 | 26 | export const putCommentSHA = (id, comment) => reqSingle(`/api/essays/${splashEnd(id)}`, 'PUT', { comment }); 27 | 28 | export const putCommentId = (id, comment) => reqSingle(`/api/essays/${id}.json`, 'PUT', { comment }); 29 | 30 | export const putLikeSHA = id => reqSingle(`/api/essay_like/${splashEnd(id)}`, 'PUT', { action: 'like' }); 31 | 32 | export const putUnlikeSHA = id => reqSingle(`/api/essay_like/${splashEnd(id)}`, 'PUT', { action: 'unlike' }); 33 | -------------------------------------------------------------------------------- /src/apis/FeedBackApis.js: -------------------------------------------------------------------------------- 1 | import { req } from '../apis/util'; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const createFeedBack = (email, rating, feedback) => req('/api/feedback/', 'POST', {}, 5 | { text: JSON.stringify({ email, rating, feedback }) }); 6 | -------------------------------------------------------------------------------- /src/apis/MindTableEditorApis.js: -------------------------------------------------------------------------------- 1 | import { req, reqSingle } from '../apis/util'; 2 | 3 | // eslint-disable-next-line import/prefer-default-export,camelcase,max-len 4 | export const createMTdata = (title, abbrtitle, author, url, journal, years, volume, pages, read_state, ref) => req( 5 | '/api/articles/', 'POST', {}, 6 | // eslint-disable-next-line max-len 7 | { title, abbrtitle, author, url, journal, years, volume, pages, read_state, article_references: ref }); 8 | 9 | export const deleteMTdata = id => req( 10 | `api/articles/${id}/`, 'DELETE'); 11 | 12 | export const changeMTdata = data => req( 13 | `api/articles/${data.id}/`, 'PATCH', {}, data); 14 | 15 | export const getAllNewPapers = () => req('/api/newpapers/', 'GET', {}); 16 | 17 | export const getPaperCertainInterest = interest => req(`/api/newpaper/${interest}`, 'GET', {}); 18 | 19 | export const getUserDetails = () => req('/api/users/', 'GET', {}); 20 | 21 | export const getArticle = id => reqSingle(`/api/articles/${id}/`, 'GET'); 22 | 23 | export const updateAlias = (id, alias) => reqSingle(`/api/articles/${id}/`, 'PATCH', { alias }); 24 | -------------------------------------------------------------------------------- /src/apis/RoadmapEditorApis.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { req, reqSingle } from '../apis/util'; 3 | 4 | function splashEnd(id) { 5 | let idSplash = id; 6 | if (!_(id).endsWith('/')) { 7 | idSplash = `${id}/`; 8 | } 9 | return idSplash; 10 | } 11 | 12 | export const createRoadmap = (roadmapTitle, nodes, connections, refConnections, description, nextNodeId, thumbnail, articles, essays) => req('/api/road_maps/', 'POST', {}, 13 | { text: JSON.stringify({ nodes, connections, refConnections, nextNodeId, thumbnail }), 14 | title: roadmapTitle, 15 | description, 16 | articles, 17 | essays, 18 | }); 19 | 20 | export const updateRoadmap = (id, roadmapTitle, nodes, connections, refConnections, description, nextNodeId, thumbnail, bindEssay, articles, essays) => req(`/api/road_maps/${id}.json`, 'PUT', {}, 21 | { text: JSON.stringify({ nodes, connections, refConnections, nextNodeId, thumbnail, bindEssay }), 22 | title: roadmapTitle, 23 | description, 24 | articles, 25 | essays, 26 | }); 27 | 28 | export const getRoadmap = id => req(`/api/road_maps/${id}.json`, 'GET'); 29 | 30 | export const updateRoadmapTitle = (id, roadmapTitle) => req(`/api/road_maps/${id}.json`, 'PATCH', {}, 31 | { title: roadmapTitle }); 32 | 33 | export const updateRoadmapDescription = (id, roadmapDescription) => req(`/api/road_maps/${id}.json`, 'PATCH', {}, 34 | { description: roadmapDescription }); 35 | 36 | export const delRoadmap = id => req(`/api/road_maps/${id}.json`, 'DELETE'); 37 | 38 | export const postRoadmapShareLink = id => req('/api/share/roadmap/', 'POST', {}, { id }); 39 | 40 | export const getRoadmapShareLink = id => req(`/api/share/roadmap/${splashEnd(id)}`, 'GET'); 41 | 42 | export const updateRoadmapTag = (id, tag) => req(`/api/road_maps/${id}.json`, 'PATCH', {}, { tag }); 43 | 44 | export const createTag = name => req('/api/tags/', 'POST', {}, { name }); 45 | 46 | export const createComment = text => reqSingle('/api/comments/', 'POST', { text }); 47 | 48 | export const putCommentSHA = (id, comment) => reqSingle(`/api/road_maps/${splashEnd(id)}`, 'PUT', { comment }); 49 | 50 | export const putCommentId = (id, comment) => reqSingle(`/api/road_maps/${splashEnd(id)}.json`, 'PUT', { comment }); 51 | 52 | export const putLikeSHA = id => reqSingle(`/api/roadmap_like/${splashEnd(id)}`, 'PUT', { action: 'like' }); 53 | 54 | export const putUnlikeSHA = id => reqSingle(`/api/roadmap_like/${splashEnd(id)}`, 'PUT', { action: 'unlike' }); 55 | -------------------------------------------------------------------------------- /src/apis/User.js: -------------------------------------------------------------------------------- 1 | import store from '../vuex/index'; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const logout = () => { 5 | store.commit('pushAuthToken', ''); 6 | }; 7 | 8 | export const isLogin = () => !(store.state.authToken === '' 9 | || store.state.authToken === undefined 10 | || store.state.authToken === null); 11 | -------------------------------------------------------------------------------- /src/apis/UserProfileApis.js: -------------------------------------------------------------------------------- 1 | import { reqSingle } from './util'; 2 | 3 | export const updateUserName = (id, username) => reqSingle(`/api/users/${id}.json`, 'PATCH', { username }); 4 | 5 | export const updateUserEmail = (id, email) => reqSingle(`/api/users/${id}.json`, 'PUT', { email }); 6 | 7 | export const updateUserCity = (id, city) => reqSingle(`/api/users/${id}.json`, 'PUT', { city }); 8 | 9 | export const updateUserOrgan = (id, organization) => reqSingle(`/api/users/${id}.json`, 'PUT', { organization }); 10 | 11 | export const updateUserBio = (id, bio) => reqSingle(`/api/users/${id}.json`, 'PUT', { bio }); 12 | 13 | export const updateInterest = (id, interest) => reqSingle(`/api/users/${id}.json`, 'PUT', { interest }); 14 | 15 | export const getUser = () => reqSingle('/api/users/', 'GET'); 16 | -------------------------------------------------------------------------------- /src/apis/util.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import store from '../vuex/index'; 3 | import router from '../router'; 4 | 5 | // axios with interceptor 6 | // eslint-disable-next-line no-unused-vars 7 | const instanceNoAuth = axios.create({ 8 | headers: { 9 | 'Access-Control-Allow-Origin': '*', 10 | }, 11 | }); 12 | 13 | instanceNoAuth.interceptors.response.use( 14 | response => response, 15 | (error) => { 16 | if (error.response) { 17 | // code: 错误状态码, response:错误响应信息,error:原始错误信息 18 | switch (error.response.status) { 19 | case 400: 20 | return Promise.reject({ code: 4000, response: error.response, error }); // 客户端请求有语法错误 21 | case 401: 22 | return Promise.reject({ code: 4010, response: error.response, error }); // 请求未经授权 23 | case 404: 24 | return Promise.reject({ code: 4040, response: error.response, error }); // 页面未找到 25 | case 403: 26 | return Promise.reject({ code: 4030, response: error.response, error }); // Bad Gateway 27 | case 500: 28 | return Promise.reject({ code: 5000, response: error.response, error }); // Server Error 29 | default: 30 | return Promise.reject({ code: -1, response: error.response, error }); // 不常见错误 31 | } 32 | } else { 33 | return Promise.reject({ code: -1, response: {}, error }); 34 | } 35 | }, 36 | ); 37 | 38 | // eslint-disable-next-line no-unused-vars 39 | const instanceAuth = axios.create({ 40 | headers: { 41 | 'Access-Control-Allow-Origin': '*', 42 | }, 43 | }); 44 | 45 | instanceAuth.interceptors.request.use( 46 | (config) => { 47 | const configTemp = config; 48 | if (store.state.authToken) { 49 | configTemp.headers.Authorization = `JWT ${store.state.authToken}`; 50 | } 51 | // configTemp.headers.Authorization = store.state.authToken.toString(); 52 | return configTemp; 53 | }, 54 | error => Promise.reject(error), 55 | ); 56 | 57 | instanceAuth.interceptors.response.use( 58 | response => response, 59 | (error) => { 60 | if (error.response) { 61 | // code: 错误状态码, response:错误响应信息,error:原始错误信息 62 | switch (error.response.status) { 63 | case 400: 64 | return Promise.reject({ code: 4000, response: error.response, error }); // 客户端请求有语法错误 65 | case 401: // 请求未经授权 66 | { 67 | store.commit('pushAuthToken', ''); 68 | router.push({ name: 'Login' }); 69 | return Promise.reject({ code: 4010, response: error.response, error }); 70 | } 71 | case 404: 72 | return Promise.reject({ code: 4040, response: error.response, error }); // 页面未找到 73 | case 403: 74 | return Promise.reject({ code: 4030, response: error.response, error }); // Bad Gateway 75 | case 500: 76 | return Promise.reject({ code: 5000, response: error.response, error }); // Server Error 77 | default: 78 | return Promise.reject({ code: -1, response: error.response, error }); // 不常见错误 79 | } 80 | } else { 81 | return Promise.reject({ code: -1, response: {}, error }); 82 | } 83 | }, 84 | ); 85 | 86 | // 需要认证Token的Request方法 87 | // eslint-disable-next-line import/prefer-default-export 88 | export const req = (url, _method, params = {}, data = {}) => { 89 | const method = _method.toUpperCase(); 90 | let options; 91 | if (method === 'POST' || method === 'PUT' || method === 'PATCH') { 92 | options = { 93 | method, 94 | url, 95 | params, 96 | data, 97 | }; 98 | } else { 99 | options = { 100 | method, 101 | url, 102 | params, 103 | }; 104 | } 105 | return instanceAuth(options); 106 | }; 107 | 108 | // 需要认证Token的Request方法,将data和params两个参数进行了合并。 109 | // eslint-disable-next-line camelcase,no-undef 110 | export const reqSingle = (url, _method, params_or_data = {}) => { 111 | const method = _method.toUpperCase(); 112 | let options; 113 | if (method === 'POST' || method === 'PUT' || method === 'PATCH') { 114 | options = { 115 | method, 116 | url, 117 | data: params_or_data, 118 | }; 119 | } else { 120 | options = { 121 | method, 122 | url, 123 | params: params_or_data, 124 | }; 125 | } 126 | return instanceAuth(options); 127 | }; 128 | 129 | // 无需认证Token的Request方法。 130 | // eslint-disable-next-line camelcase 131 | export const reqNoAuth = (url, _method, params_or_data = {}) => { 132 | const method = _method.toUpperCase(); 133 | let options; 134 | if (method === 'POST' || method === 'PUT' || method === 'PATCH') { 135 | options = { 136 | method, 137 | url, 138 | data: params_or_data, 139 | }; 140 | } else { 141 | options = { 142 | method, 143 | url, 144 | params: params_or_data, 145 | }; 146 | } 147 | return instanceNoAuth(options); 148 | }; 149 | -------------------------------------------------------------------------------- /src/assets/HomePageLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/HomePageLogo.png -------------------------------------------------------------------------------- /src/assets/LoginLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/LoginLogo.png -------------------------------------------------------------------------------- /src/assets/RoadmapDefault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/RoadmapDefault.png -------------------------------------------------------------------------------- /src/assets/css/emoji.css: -------------------------------------------------------------------------------- 1 | .emoji-yum { 2 | width: 64px; 3 | height: 64px; 4 | background-position: -0px -0px !important; 5 | } 6 | 7 | .emoji-angry { 8 | width: 64px; 9 | height: 64px; 10 | background-position: -64px -0px !important; 11 | } 12 | 13 | .emoji-anguished { 14 | width: 64px; 15 | height: 64px; 16 | background-position: -128px -0px !important; 17 | } 18 | 19 | .emoji-astonished { 20 | width: 64px; 21 | height: 64px; 22 | background-position: -192px -0px !important; 23 | } 24 | 25 | .emoji-cold_sweat { 26 | width: 64px; 27 | height: 64px; 28 | background-position: -256px -0px !important; 29 | } 30 | 31 | .emoji-cry { 32 | width: 64px; 33 | height: 64px; 34 | background-position: -320px -0px !important; 35 | } 36 | 37 | .emoji-blush { 38 | width: 64px; 39 | height: 64px; 40 | background-position: -384px -0px !important; 41 | } 42 | 43 | .emoji-confounded { 44 | width: 64px; 45 | height: 64px; 46 | background-position: -448px -0px !important; 47 | } 48 | 49 | .emoji-dizzy_face { 50 | width: 64px; 51 | height: 64px; 52 | background-position: -512px -0px !important; 53 | } 54 | 55 | .emoji-flushed { 56 | width: 64px; 57 | height: 64px; 58 | background-position: -0px -64px !important; 59 | } 60 | 61 | .emoji-fearful { 62 | width: 64px; 63 | height: 64px; 64 | background-position: -64px -64px !important; 65 | } 66 | 67 | .emoji-grimacing { 68 | width: 64px; 69 | height: 64px; 70 | background-position: -128px -64px !important; 71 | } 72 | 73 | .emoji-disappointed_relieved { 74 | width: 64px; 75 | height: 64px; 76 | background-position: -192px -64px !important; 77 | } 78 | 79 | .emoji-disappointed { 80 | width: 64px; 81 | height: 64px; 82 | background-position: -256px -64px !important; 83 | } 84 | 85 | .emoji-expressionless { 86 | width: 64px; 87 | height: 64px; 88 | background-position: -320px -64px !important; 89 | } 90 | 91 | .emoji-innocent { 92 | width: 64px; 93 | height: 64px; 94 | background-position: -384px -64px !important; 95 | } 96 | 97 | .emoji-grin { 98 | width: 64px; 99 | height: 64px; 100 | background-position: -448px -64px !important; 101 | } 102 | 103 | .emoji-frowning { 104 | width: 64px; 105 | height: 64px; 106 | background-position: -512px -64px !important; 107 | } 108 | 109 | .emoji-hushed { 110 | width: 64px; 111 | height: 64px; 112 | background-position: -0px -128px !important; 113 | } 114 | 115 | .emoji-open_mouth { 116 | width: 64px; 117 | height: 64px; 118 | background-position: -64px -128px !important; 119 | } 120 | 121 | .emoji-pensive { 122 | width: 64px; 123 | height: 64px; 124 | background-position: -128px -128px !important; 125 | } 126 | 127 | .emoji-sleepy { 128 | width: 64px; 129 | height: 64px; 130 | background-position: -192px -128px !important; 131 | } 132 | 133 | .emoji-confused { 134 | width: 64px; 135 | height: 64px; 136 | background-position: -256px -128px !important; 137 | } 138 | 139 | .emoji-persevere { 140 | width: 64px; 141 | height: 64px; 142 | background-position: -320px -128px !important; 143 | } 144 | 145 | .emoji-relieved { 146 | width: 64px; 147 | height: 64px; 148 | background-position: -384px -128px !important; 149 | } 150 | 151 | .emoji-scream { 152 | width: 64px; 153 | height: 64px; 154 | background-position: -448px -128px !important; 155 | } 156 | 157 | .emoji-stuck_out_tongue_winking_eye { 158 | width: 64px; 159 | height: 64px; 160 | background-position: -512px -128px !important; 161 | } 162 | 163 | .emoji-stuck_out_tongue { 164 | width: 64px; 165 | height: 64px; 166 | background-position: -0px -192px !important; 167 | } 168 | 169 | .emoji-satisfied { 170 | width: 64px; 171 | height: 64px; 172 | background-position: -64px -192px !important; 173 | } 174 | 175 | .emoji-no_mouth { 176 | width: 64px; 177 | height: 64px; 178 | background-position: -128px -192px !important; 179 | } 180 | 181 | .emoji-smiley { 182 | width: 64px; 183 | height: 64px; 184 | background-position: -192px -192px !important; 185 | } 186 | 187 | .emoji-smile { 188 | width: 64px; 189 | height: 64px; 190 | background-position: -256px -192px !important; 191 | } 192 | 193 | .emoji-stuck_out_tongue_closed_eyes { 194 | width: 64px; 195 | height: 64px; 196 | background-position: -320px -192px !important; 197 | } 198 | 199 | .emoji-sweat { 200 | width: 64px; 201 | height: 64px; 202 | background-position: -384px -192px !important; 203 | } 204 | 205 | .emoji-joy { 206 | width: 64px; 207 | height: 64px; 208 | background-position: -448px -192px !important; 209 | } 210 | 211 | .emoji-heart_eyes { 212 | width: 64px; 213 | height: 64px; 214 | background-position: -512px -192px !important; 215 | } 216 | 217 | .emoji-grinning { 218 | width: 64px; 219 | height: 64px; 220 | background-position: -0px -256px !important; 221 | } 222 | 223 | .emoji-smirk { 224 | width: 64px; 225 | height: 64px; 226 | background-position: -64px -256px !important; 227 | } 228 | 229 | .emoji-relaxed { 230 | width: 64px; 231 | height: 64px; 232 | background-position: -128px -256px !important; 233 | } 234 | 235 | .emoji-kissing_closed_eyes { 236 | width: 64px; 237 | height: 64px; 238 | background-position: -192px -256px !important; 239 | } 240 | 241 | .emoji-sunglasses { 242 | width: 64px; 243 | height: 64px; 244 | background-position: -256px -256px !important; 245 | } 246 | 247 | .emoji-sweat_smile { 248 | width: 64px; 249 | height: 64px; 250 | background-position: -320px -256px !important; 251 | } 252 | 253 | .emoji-sob { 254 | width: 64px; 255 | height: 64px; 256 | background-position: -384px -256px !important; 257 | } 258 | 259 | .emoji-wink { 260 | width: 64px; 261 | height: 64px; 262 | background-position: -448px -256px !important; 263 | } 264 | 265 | .emoji-worried { 266 | width: 64px; 267 | height: 64px; 268 | background-position: -512px -256px !important; 269 | } 270 | 271 | .emoji-neutral_face { 272 | width: 64px; 273 | height: 64px; 274 | background-position: -0px -320px !important; 275 | } 276 | 277 | .emoji-unamused { 278 | width: 64px; 279 | height: 64px; 280 | background-position: -64px -320px !important; 281 | } 282 | 283 | .emoji-weary { 284 | width: 64px; 285 | height: 64px; 286 | background-position: -128px -320px !important; 287 | } 288 | 289 | .emoji-tired_face { 290 | width: 64px; 291 | height: 64px; 292 | background-position: -192px -320px !important; 293 | } 294 | 295 | .emoji-triumph { 296 | width: 64px; 297 | height: 64px; 298 | background-position: -256px -320px !important; 299 | } 300 | 301 | .emoji-laughing { 302 | width: 64px; 303 | height: 64px; 304 | background-position: -320px -320px !important; 305 | } 306 | 307 | .emoji-kissing_heart { 308 | width: 64px; 309 | height: 64px; 310 | background-position: -384px -320px !important; 311 | } 312 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/emoji_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/img/emoji_sprite.png -------------------------------------------------------------------------------- /src/assets/img/face_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/img/face_logo.png -------------------------------------------------------------------------------- /src/assets/img/heading.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/img/heading.jpg -------------------------------------------------------------------------------- /src/assets/login/input_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/login/input_password.png -------------------------------------------------------------------------------- /src/assets/login/input_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/login/input_user.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/welcome/books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/welcome/books.png -------------------------------------------------------------------------------- /src/assets/welcome/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/welcome/folder.png -------------------------------------------------------------------------------- /src/assets/welcome/mindmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/welcome/mindmap.png -------------------------------------------------------------------------------- /src/assets/welcome/recommand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/src/assets/welcome/recommand.png -------------------------------------------------------------------------------- /src/components/AddCommentForm.vue: -------------------------------------------------------------------------------- 1 | 18 | 44 | -------------------------------------------------------------------------------- /src/components/AddConnectionForm.vue: -------------------------------------------------------------------------------- 1 | 22 | 50 | -------------------------------------------------------------------------------- /src/components/AddNodeForm.vue: -------------------------------------------------------------------------------- 1 | 22 | 50 | -------------------------------------------------------------------------------- /src/components/ArticleStatisExpand.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 37 | 38 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/ArticleStatistics.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 301 | 302 | 317 | -------------------------------------------------------------------------------- /src/components/Breadcrumb.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-array-constructor 2 | window.routerStack = new Array(); 3 | window.routerStack.push({ name: 'Welcome', nickName: '主页', fullPath: '/', level: 1 }); 4 | 5 | export default function pushRouter(name, nickName, fullPath, level) { 6 | let hasHomePage = false; 7 | for (let i = 0; i < window.routerStack.length; i += 1) { 8 | if (window.routerStack[i].nickName === '主页') { 9 | hasHomePage = true; 10 | } else if ((window.routerStack[i].level >= level || window.routerStack[i].name === undefined)) { 11 | window.routerStack.splice(i, 1); 12 | i -= 1; 13 | } 14 | } 15 | if (!hasHomePage) { 16 | window.routerStack.push({ name: 'Welcome', nickName: '主页', fullPath: '/', level: 1 }); 17 | } 18 | if (level > 1) { 19 | window.routerStack.push({ name, nickName, fullPath, level }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/DelCommentForm.vue: -------------------------------------------------------------------------------- 1 | 22 | 63 | -------------------------------------------------------------------------------- /src/components/DelConnectionForm.vue: -------------------------------------------------------------------------------- 1 | 22 | 50 | -------------------------------------------------------------------------------- /src/components/DelNodeForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 43 | -------------------------------------------------------------------------------- /src/components/EditRoadmapDescriptionForm.vue: -------------------------------------------------------------------------------- 1 | 13 | 37 | -------------------------------------------------------------------------------- /src/components/ErrPush.js: -------------------------------------------------------------------------------- 1 | /* 2 | * How to use? 3 | * import errPush from './components/ErrHandler'; 4 | * @errCode: string 5 | * @useModal: boolean, default to true 6 | * */ 7 | 8 | export default function pushError(obj, errCode, useModal = false, errTitle = '', errContent = '') { 9 | window.console.warn('Deprecated method. Please use pushErr'); 10 | let title = ''; 11 | let content = ''; 12 | if (errTitle === '' && errContent === '') { 13 | if (errCode === '0001') { 14 | title = 'DEBUG ERROR '; 15 | content = '调试用错误'; 16 | } else if (errCode === '0010') { 17 | title = 'Unfinished Exception'; 18 | content = '未实现的方法或接口'; 19 | } else if (errCode === '1000') { 20 | title = 'Runtime Exception'; 21 | content = '运行时错误'; 22 | } else if (errCode === '1010') { 23 | title = 'Undefined Object'; 24 | content = '访问未定义对象'; 25 | } else if (errCode === '4000') { 26 | title = 'Unclassified Network Exception'; 27 | content = '无法分类的网络错误'; 28 | } else if (errCode === '4040') { 29 | title = 'Resources NOT Exist Exception'; 30 | content = '访问或查询不存在的资源'; 31 | } else if (errCode === '4010') { 32 | title = 'Unauthorized Exception'; 33 | content = '未登陆或没有访问权限'; 34 | } else if (errCode === '5000') { 35 | title = 'Server Error'; 36 | content = '服务器内部错误'; 37 | } else if (errCode === '5010') { 38 | title = 'Incomplete Input'; 39 | content = '输入不完整'; 40 | } else if (errCode === '5020') { 41 | title = 'Login Fail'; 42 | content = '登陆失败'; 43 | } else { 44 | return; 45 | } 46 | } else { 47 | title = errTitle; 48 | content = errContent; 49 | } 50 | 51 | window.console.error('[ERROR] ', title, ': ', content); 52 | 53 | if (useModal) { 54 | obj.$Modal.error({ 55 | title, 56 | content, 57 | okText: '知道了~', 58 | }); 59 | } else { 60 | title += ': '; 61 | title += content; 62 | obj.$Message.error({ 63 | background: true, 64 | content: title, 65 | duration: 5, 66 | closable: true, 67 | }); 68 | } 69 | } 70 | 71 | export const pushErr = (obj, err, useModal = false, errTitle = '', errContent = '', modalType = 'error') => { 72 | let title = ''; 73 | let content = ''; 74 | let errCode = ''; 75 | let response = ''; 76 | window.console.log(err); 77 | if (typeof err === 'string') { 78 | errCode = err; 79 | } else { 80 | errCode = String(err.code); 81 | response = err.response.data.detail; 82 | if (!response) { 83 | response = err.response.data.non_field_errors; 84 | if (response) { 85 | response = '用户名或密码错误'; 86 | } 87 | } 88 | if (!response) { 89 | response = err.response.data.username; 90 | if (response) { 91 | // eslint-disable-next-line no-param-reassign 92 | errTitle = '注册错误'; 93 | // eslint-disable-next-line no-param-reassign 94 | errContent = '该用户名已存在'; 95 | } 96 | } 97 | } 98 | if (errTitle === '' && errContent === '') { 99 | if (errCode === '0001') { 100 | title = 'DEBUG ERROR '; 101 | content = '调试用错误'; 102 | } else if (errCode === '0010') { 103 | title = 'Unfinished Exception'; 104 | content = '未实现的方法或接口'; 105 | } else if (errCode === '1000') { 106 | title = 'Runtime Exception'; 107 | content = '运行时错误'; 108 | } else if (errCode === '1010') { 109 | title = 'Undefined Object'; 110 | content = '访问未定义对象'; 111 | } else if (errCode === '4000') { 112 | title = 'Unclassified Network Exception'; 113 | content = '无法分类的网络错误'; 114 | } else if (errCode === '4030') { 115 | title = 'Permission Denied'; 116 | content = '没有权限'; 117 | } else if (errCode === '4040') { 118 | title = 'Resources NOT Exist Exception'; 119 | content = '访问或查询不存在的资源'; 120 | } else if (errCode === '4010') { 121 | title = 'Unauthorized Exception'; 122 | content = '请先登陆'; 123 | // eslint-disable-next-line no-param-reassign 124 | modalType = 'info'; 125 | } else if (errCode === '5010') { 126 | title = 'Incomplete Input'; 127 | content = '用户名/密码输入不完整'; 128 | } else if (errCode === '5020') { 129 | title = 'Login Fail'; 130 | content = '登陆失败'; 131 | } else { 132 | return; 133 | } 134 | } else { 135 | title = errTitle; 136 | content = errContent; 137 | } 138 | window.console.error('[ERROR] ', title, ': ', content); 139 | 140 | if (useModal) { 141 | if (modalType === 'info') { 142 | obj.$Modal.info({ 143 | title, 144 | content: `${content}
${typeof response !== 'undefined' ? response : ''}`, 145 | okText: '知道了~', 146 | }); 147 | } else { 148 | obj.$Modal.error({ 149 | title, 150 | content: `${content}
${typeof response !== 'undefined' ? response : ''}`, 151 | okText: '知道了~', 152 | }); 153 | } 154 | } else { 155 | title += ': '; 156 | title += content; 157 | obj.$Message.error({ 158 | background: true, 159 | content: title, 160 | duration: 5, 161 | closable: true, 162 | }); 163 | } 164 | }; 165 | -------------------------------------------------------------------------------- /src/components/EssayTable.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 263 | 264 | 267 | -------------------------------------------------------------------------------- /src/components/EssayTableExpand.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /src/components/FileItem.vue: -------------------------------------------------------------------------------- 1 | 22 | 51 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 96 | 97 | 98 | 114 | -------------------------------------------------------------------------------- /src/components/Likes.vue: -------------------------------------------------------------------------------- 1 | 26 | 56 | -------------------------------------------------------------------------------- /src/components/LoadRoadmapForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 42 | -------------------------------------------------------------------------------- /src/components/MindTableExpand.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 50 | 51 | 56 | -------------------------------------------------------------------------------- /src/components/ModifyAliasForm.vue: -------------------------------------------------------------------------------- 1 | 27 | 57 | -------------------------------------------------------------------------------- /src/components/ModifyColor.vue: -------------------------------------------------------------------------------- 1 | 17 | 43 | -------------------------------------------------------------------------------- /src/components/ModifyCommentForm.vue: -------------------------------------------------------------------------------- 1 | 18 | 45 | -------------------------------------------------------------------------------- /src/components/ModifyNodeForm.vue: -------------------------------------------------------------------------------- 1 | 20 | 50 | -------------------------------------------------------------------------------- /src/components/NoteMarkdown.vue: -------------------------------------------------------------------------------- 1 | 16 | 60 | -------------------------------------------------------------------------------- /src/components/PaperRecommend.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 109 | 110 | 115 | -------------------------------------------------------------------------------- /src/components/Request.vue: -------------------------------------------------------------------------------- 1 | git 11 | 12 | 40 | -------------------------------------------------------------------------------- /src/components/RoadItemEditor.vue: -------------------------------------------------------------------------------- 1 | 67 | 149 | 161 | 162 | -------------------------------------------------------------------------------- /src/components/RoadmapTable.vue: -------------------------------------------------------------------------------- 1 | 20 | 232 | 233 | 236 | -------------------------------------------------------------------------------- /src/components/TableItemEditor.vue: -------------------------------------------------------------------------------- 1 | 165 | 265 | 277 | 278 | -------------------------------------------------------------------------------- /src/components/UserReportButton.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /src/components/comment/Comment.vue: -------------------------------------------------------------------------------- 1 | 22 | 58 | 183 | -------------------------------------------------------------------------------- /src/components/comment/children/Emoji.vue: -------------------------------------------------------------------------------- 1 | 6 | 20 | -------------------------------------------------------------------------------- /src/components/comment/children/EmojiPanel.vue: -------------------------------------------------------------------------------- 1 | 11 | 83 | 96 | -------------------------------------------------------------------------------- /src/components/roadmap/Roadmap.vue: -------------------------------------------------------------------------------- 1 | 19 | 285 | -------------------------------------------------------------------------------- /src/components/roadmap/parser/emojis.js: -------------------------------------------------------------------------------- 1 | // Regex that matches all emojis in a string. 2 | export const matchEmojis = /([\uD800-\uDBFF][\uDC00-\uDFFF])/g; 3 | 4 | // Emoji to category table. 5 | const conversionTable = { 6 | '🗺': 'mindmap', 7 | '🌐': 'wiki', 8 | '🗂': 'stack exchange', 9 | '📝': 'course', 10 | '📖': 'free book', 11 | '📕': 'non-free book', 12 | '📄': 'paper', 13 | '👀': 'video', 14 | '🖋': 'article', 15 | '🗃': 'blog', 16 | '🐙': 'github', 17 | '👾': 'interactive', 18 | '🖌': 'image', 19 | '🎙': 'podcast', 20 | '📮': 'newsletter', 21 | '💬': 'chat', 22 | '🎥': 'youtube', 23 | '🤖': 'reddit', 24 | '🔎': 'quora', 25 | '🔗': undefined, 26 | }; 27 | 28 | // Category to emoji table, based on the table above. 29 | const revConversionTable = {}; 30 | 31 | Object.keys(conversionTable).forEach((key) => { 32 | revConversionTable[conversionTable[key]] = key; 33 | }); 34 | 35 | /* 36 | * Return an emoji as a GitHub image. 37 | */ 38 | export const emojiTemplate = (unicode, category) => ( 39 | `` 40 | ); 41 | 42 | export const customEmojiTemplate = (emoji, category) => ( 43 | `` 44 | ); 45 | 46 | /* 47 | * Return the category represented by the given emoji. 48 | */ 49 | export const emojiToCategory = emoji => conversionTable[emoji] || ''; 50 | 51 | /* 52 | * Convert all emojis to an IMG tag. 53 | * The bitwise magic is explained at http://crocodillon.com/blog/parsing-emoji-unicode-in-javascript 54 | */ 55 | export const emojiToIMG = html => ( 56 | /* eslint-disable no-bitwise */ 57 | html.replace(matchEmojis, (match) => { 58 | switch (match) { 59 | case '🤖': 60 | return ''; 61 | 62 | case '🗂': 63 | return ''; 64 | 65 | case '🐙': 66 | return customEmojiTemplate('octocat', 'github'); 67 | 68 | case '🔎': 69 | return ''; 70 | 71 | // Regular unicode Emojis. 72 | default: { 73 | // Keep the first 10 bits. 74 | const lead = match.charCodeAt(0) & 0x3FF; 75 | const trail = match.charCodeAt(1) & 0x3FF; 76 | 77 | // 0x[lead][trail] 78 | const unicode = ((lead << 10) + trail).toString(16); 79 | 80 | return emojiTemplate(`1${unicode}`, emojiToCategory(match)); 81 | } 82 | } 83 | }) 84 | /* eslint-enable no-bitwise */ 85 | ); 86 | 87 | /* 88 | * Inverse of emojiToCategory, but instead of returning an emoji 89 | * returns an IMG tag corresponding to that emoji. 90 | */ 91 | export const categoryToIMG = category => emojiToIMG(revConversionTable[category] || ''); 92 | 93 | -------------------------------------------------------------------------------- /src/components/roadmap/parser/regex.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Extract text from the inner HTML of a node. 3 | */ 4 | const getText = (html) => { 5 | const res = []; 6 | 7 | // Match all text inside A tags. If there's no A tags, 8 | // match text inside P tags instead. 9 | const matchText = /]*>([^<]*)<\/a>|]*>([^>]*)<\/p>/g; 10 | let match = matchText.exec(html); 11 | 12 | while (match) { 13 | res.push(match[1] || match[2]); 14 | match = matchText.exec(html); 15 | } 16 | 17 | return res.join(' '); 18 | }; 19 | 20 | /* 21 | * Extract HREF content from the first link on a node. 22 | */ 23 | const getURL = (html) => { 24 | // Match HREF content inside A tags. 25 | const matchURL = /]*href="([^"]*)"[^>]*>[^<]*<\/a>/; 26 | const match = matchURL.exec(html); 27 | 28 | if (match) { 29 | return match[1]; 30 | } 31 | 32 | return ''; 33 | }; 34 | 35 | module.exports = { 36 | getText, 37 | getURL, 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/roadmap/style.sass: -------------------------------------------------------------------------------- 1 | $grey100: #f5f5f5 2 | $grey500: #9e9e9e 3 | $grey800: #424242 4 | $grey900: #212121 5 | $orange700: #f57c00 6 | $yellow: #f5d72b 7 | $white: #fff 8 | $shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12) 9 | 10 | 11 | .mindmap-svg 12 | height: 100vh 13 | width: 100% 14 | 15 | &:focus 16 | outline: none 17 | 18 | .mindmap-node > a 19 | max-width: 500px 20 | height: auto 21 | border-radius: 10px 22 | box-shadow: $shadow 23 | color: $grey900 24 | display: inline-block 25 | font-family: 'Raleway' 26 | font-size: 22px 27 | margin: 0 auto 28 | padding: 10px 29 | text-align: center 30 | text-decoration: none 31 | transition: background-color .2s, color .2s ease-out 32 | 33 | &[href] 34 | &:hover 35 | background-color: $orange700 36 | color: $white 37 | cursor: pointer 38 | 39 | .mindmap-node-chosen > a 40 | max-width: 500px 41 | height: auto 42 | border: dashed 43 | border-width: 2px 44 | border-radius: 10px 45 | box-shadow: $shadow 46 | color: $grey900 47 | display: inline-block 48 | font-family: 'Raleway' 49 | font-size: 22px 50 | margin: 0 auto 51 | padding: 10px 52 | text-align: center 53 | text-decoration: none 54 | transition: background-color .2s, color .2s ease-out 55 | 56 | &[href] 57 | &:hover 58 | background-color: $orange700 59 | color: $white 60 | cursor: pointer 61 | 62 | .article-node > a 63 | max-width: 500px 64 | height: auto 65 | border-radius: 10px 66 | box-shadow: $shadow 67 | color: $grey900 68 | display: inline-block 69 | font-family: 'Raleway' 70 | font-size: 22px 71 | margin: 0 auto 72 | padding: 10px 73 | text-align: center 74 | text-decoration: none 75 | transition: background-color .2s, color .2s ease-out 76 | 77 | &[href] 78 | &:hover 79 | background-color: $orange700 80 | color: $white 81 | cursor: pointer 82 | 83 | .article-node-chosen > a 84 | max-width: 500px 85 | height: auto 86 | border: dashed 87 | border-width: 2px 88 | border-radius: 10px 89 | box-shadow: $shadow 90 | color: $grey900 91 | display: inline-block 92 | font-family: 'Raleway' 93 | font-size: 22px 94 | margin: 0px auto 95 | padding: 10px 96 | text-align: center 97 | text-decoration: none 98 | transition: background-color .2s, color .2s ease-out 99 | 100 | 101 | &[href] 102 | &:hover 103 | background-color: $orange700 104 | color: $white 105 | cursor: pointer 106 | 107 | .essay-node > a 108 | max-width: 500px 109 | height: auto 110 | border-radius: 10px 111 | box-shadow: $shadow 112 | color: $grey900 113 | display: inline-block 114 | font-family: 'Raleway' 115 | font-size: 22px 116 | margin: 0 auto 117 | padding: 10px 118 | text-align: center 119 | text-decoration: none 120 | transition: background-color .2s, color .2s ease-out 121 | 122 | &[href] 123 | &:hover 124 | background-color: $orange700 125 | color: $white 126 | cursor: pointer 127 | 128 | .essay-node-chosen > a 129 | max-width: 500px 130 | height: auto 131 | border: dashed 132 | border-width: 2px 133 | border-radius: 10px 134 | box-shadow: $shadow 135 | color: $grey900 136 | display: inline-block 137 | font-family: 'Raleway' 138 | font-size: 22px 139 | margin: 0px auto 140 | padding: 10px 141 | text-align: center 142 | text-decoration: none 143 | transition: background-color .2s, color .2s ease-out 144 | 145 | 146 | &[href] 147 | &:hover 148 | background-color: $orange700 149 | color: $white 150 | cursor: pointer 151 | 152 | .mindmap-node--editable 153 | cursor: all-scroll 154 | 155 | & > a 156 | pointer-events: none 157 | 158 | .article-node--editable 159 | cursor: all-scroll 160 | 161 | & > a 162 | pointer-events: none 163 | 164 | .mindmap-subnode-group 165 | align-items: center 166 | border-left: 4px solid $grey500 167 | display: flex 168 | margin-left: 15px 169 | padding: 5px 170 | 171 | a 172 | color: $grey900 173 | font-family: 'Raleway' 174 | font-size: 16px 175 | padding: 2px 5px 176 | 177 | .mindmap-connection 178 | fill: transparent 179 | stroke: $grey500 180 | stroke-dasharray: 10px 2px 181 | stroke-width: 3px 182 | marker-end: url(#arrow) 183 | 184 | .mindmap-connection-chosen 185 | fill: transparent 186 | stroke: black 187 | stroke-dasharray: 10px 4px 188 | stroke-width: 3px 189 | marker-end: url(#arrow) 190 | 191 | .mindmap-connection-reference 192 | fill: transparent 193 | stroke: orange 194 | stroke-dasharray: 10px 2px 195 | stroke-width: 3px 196 | marker-end: url(#arrow) 197 | 198 | .mindmap-emoji 199 | height: 24px 200 | vertical-align: bottom 201 | width: 24px 202 | 203 | .reddit-emoji 204 | border-radius: 50% 205 | -------------------------------------------------------------------------------- /src/components/roadmap/utils/d3.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | 3 | import _ from 'lodash'; 4 | import { drag, event, zoom } from 'd3'; 5 | // import { getViewBox } from './dimensions'; 6 | /** 7 | * Bind data to a , inside a G element, inside the given root element. 8 | * Root is a D3 selection, data is an object or array, tag is a string. 9 | */ 10 | const bindData = (root, data, tag) => ( 11 | root.append('g') 12 | .selectAll(tag) 13 | .data(data) 14 | .enter() 15 | .append(tag) 16 | ); 17 | 18 | /** 19 | * Bind connections to PATH tags on the given SVG 20 | */ 21 | export const d3Connections = (svg, connections) => { 22 | window.console.warn('d3Connections is Deprecated. Use d3CustomConnections instead.'); 23 | return bindData(svg, connections, 'path') 24 | .attr('class', 'mindmap-connection'); 25 | }; 26 | 27 | export const d3CustomConnections = (svg, connections) => { 28 | const refConn = _.filter(connections, conn => conn.type === 'ref'); 29 | const simpleConn = _.filter(connections, conn => conn.type !== 'ref'); 30 | 31 | const refSelections = bindData(svg, refConn, 'path') 32 | .attr('class', 'mindmap-connection-reference'); 33 | const simpleSelections = bindData(svg, simpleConn, 'path') 34 | .attr('class', 'mindmap-connection'); 35 | 36 | const selections = simpleSelections; 37 | selections._groups = [_.flattenDeep(_.map([refSelections, simpleSelections], '_groups'))]; 38 | return selections; 39 | }; 40 | 41 | /* eslint-disable no-param-reassign */ 42 | /** 43 | * Bind rodes to FOREIGNOBJECT tags on the given SVG, 44 | * and set dimensions and html. 45 | */ 46 | export const d3Nodes = (svg, nodes) => { 47 | const selection = svg.append('g') 48 | .selectAll('foreignObject') 49 | .data(nodes) 50 | .enter(); 51 | 52 | const d3nodes = selection 53 | .append('foreignObject') 54 | .attr('class', 'mindmap-node') 55 | .attr('width', node => node.width + 8) 56 | .attr('height', node => node.height + 4) 57 | .html(node => node.html); 58 | 59 | const d3subnodes = selection 60 | .append('foreignObject') 61 | .attr('class', 'mindmap-subnodes') 62 | .attr('width', node => node.nodesWidth + 4) 63 | .attr('height', node => node.nodesHeight) 64 | .html(node => node.nodesHTML); 65 | 66 | return { 67 | nodes: d3nodes, 68 | subnodes: d3subnodes, 69 | }; 70 | }; 71 | 72 | /** 73 | * Callback for forceSimulation tick event. 74 | */ 75 | export const onTick = (conns, nodes, subnodes) => { 76 | const d = conn => [ 77 | 'M', 78 | conn.source.x, 79 | conn.source.y, 80 | 'C', 81 | (conn.curve && conn.curve.x ? conn.curve.x : conn.source.x), 82 | (conn.curve && conn.curve.y ? conn.curve.y : conn.source.y), 83 | ',', 84 | (conn.curve && conn.curve.x ? conn.curve.x : conn.source.x), 85 | (conn.curve && conn.curve.y ? conn.curve.y : conn.source.y), 86 | ',', 87 | conn.target.x, 88 | conn.target.y, 89 | ].join(' '); 90 | 91 | // Set the connections path. 92 | conns.attr('d', d); 93 | 94 | // Set nodes position. 95 | nodes 96 | .attr('x', node => node.x - (node.width / 2)) 97 | .attr('y', node => node.y - (node.height / 2)); 98 | 99 | // Set subnodes groups color and position. 100 | subnodes 101 | .attr('x', node => node.x + (node.width / 2)) 102 | .attr('y', node => node.y - (node.nodesHeight / 2)); 103 | }; 104 | 105 | /* 106 | * Return drag behavior to use on d3.selection.call(). 107 | */ 108 | // eslint-disable-next-line no-unused-vars 109 | export const d3Drag = (simulation, svg, nodes) => { 110 | const dragStart = (node) => { 111 | if (!event.active) { 112 | simulation.alphaTarget(0.2).restart(); 113 | } 114 | node.fx = node.x; 115 | node.fy = node.y; 116 | }; 117 | 118 | const dragged = (node) => { 119 | node.fx = event.x; 120 | node.fy = event.y; 121 | }; 122 | 123 | const dragEnd = () => { 124 | if (!event.active) { 125 | simulation.alphaTarget(0); 126 | } 127 | // svg.attr('viewBox', getViewBox(nodes.data())); 128 | }; 129 | 130 | return drag() 131 | .on('start', dragStart) 132 | .on('drag', dragged) 133 | .on('end', dragEnd); 134 | }; 135 | 136 | // eslint-disable-next-line 137 | export const d3DragConn = (simulation, svg, conns) => { 138 | const dragStart = (conn) => { 139 | if (!event.active) { 140 | simulation.alphaTarget(0.2).restart(); 141 | } 142 | if (!conn.curve) { 143 | conn.curve = { 144 | x: 0, 145 | y: 0, 146 | }; 147 | } 148 | }; 149 | 150 | const dragged = (conn) => { 151 | conn.curve.x = event.x; 152 | conn.curve.y = event.y; 153 | }; 154 | 155 | const dragEnd = () => { 156 | if (!event.active) { 157 | simulation.alphaTarget(0); 158 | } 159 | // svg.attr('viewBox', getViewBox(nodes.data())); 160 | }; 161 | 162 | return drag() 163 | .on('start', dragStart) 164 | .on('drag', dragged) 165 | .on('end', dragEnd); 166 | }; 167 | 168 | /* eslint-enable no-param-reassign */ 169 | 170 | /* 171 | * Return pan and zoom behavior to use on d3.selection.call(). 172 | */ 173 | export const d3PanZoom = el => ( 174 | zoom().scaleExtent([0.3, 5]) 175 | .on('zoom', () => ( 176 | el.selectAll('svg > g').attr('transform', event.transform) 177 | )) 178 | ); 179 | -------------------------------------------------------------------------------- /src/components/roadmap/utils/dimensions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Return the dimensions (width & height) that some HTML 3 | * with a given style would take in the page. 4 | */ 5 | export const getDimensions = (html, style, classname) => { 6 | const el = document.createElement('span'); 7 | const dimensions = {}; 8 | 9 | // Set display: inline-block so that the size of el 10 | // will depend on the size of its children. 11 | el.style.display = 'inline-block'; 12 | 13 | // Hide the element (it will be added to the page for a short time). 14 | el.style.visibility = 'hidden'; 15 | 16 | el.className = classname; 17 | el.innerHTML = html; 18 | 19 | // Apply CSS rules. 20 | Object.keys(style).forEach((rule) => { 21 | el.style[rule] = style[rule]; 22 | }); 23 | document.body.append(el); 24 | 25 | dimensions.width = el.offsetWidth; 26 | dimensions.height = el.offsetHeight; 27 | 28 | el.remove(); 29 | return dimensions; 30 | }; 31 | 32 | /* 33 | * Return the dimensions of an SVG viewport calculated from 34 | * some given nodes. 35 | */ 36 | export const getViewBox = (nodes) => { 37 | const Xs = []; 38 | const Ys = []; 39 | 40 | nodes.forEach((node) => { 41 | const x = node.x || node.fx; 42 | const y = node.y || node.fy; 43 | 44 | if (x) { 45 | Xs.push(x); 46 | } 47 | 48 | if (y) { 49 | Ys.push(y); 50 | } 51 | }); 52 | 53 | if (Xs.length === 0 || Ys.length === 0) { 54 | return '0 0 0 0'; 55 | } 56 | 57 | // Find the smallest coordinates... 58 | const min = [ 59 | Math.min(...Xs) - 150, 60 | Math.min(...Ys) - 150, 61 | ]; 62 | 63 | // ...and the biggest ones. 64 | const max = [ 65 | (Math.max(...Xs) - min[0]) + 150, 66 | (Math.max(...Ys) - min[1]) + 150, 67 | ]; 68 | 69 | return `${min.join(' ')} ${max.join(' ')}`; 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/roadmap/utils/nodeToHTML.js: -------------------------------------------------------------------------------- 1 | import { categoryToIMG } from '../parser/emojis'; 2 | 3 | /* 4 | * Return the HTML representation of a node. 5 | * The node is an object that has text, url, and category attributes 6 | * all of them optional. 7 | */ 8 | export default (node) => { 9 | let href = `href="${node.url}"`; 10 | let emoji = categoryToIMG(node.category); 11 | 12 | // If url is not specified remove the emoji and the href attribute, 13 | // so that the node isn't clickable, and the user can see that without 14 | // having to hover the node. 15 | if (!node.url) { 16 | href = ''; 17 | emoji = ''; 18 | } 19 | 20 | return `${node.content || node.text || ''} ${emoji}`; 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/roadmap/utils/subnodesToHTML.js: -------------------------------------------------------------------------------- 1 | import { categoryToIMG } from '../parser/emojis'; 2 | 3 | /* 4 | * Return the HTML representation of a node. 5 | * The node is an object that has text, url, and category attributes 6 | * all of them optional. 7 | */ 8 | const subnodesToHTML = (subnodes = [], fcolor) => { 9 | let color = fcolor || ''; 10 | 11 | if (!fcolor && subnodes.length > 0 && subnodes[0].color) { 12 | color = `style="border-left-color: ${subnodes[0].color}"`; 13 | } 14 | 15 | return subnodes.map((subnode) => { 16 | let href = `href="${subnode.url}"`; 17 | let emoji = categoryToIMG(subnode.category); 18 | 19 | if (!subnode.url) { 20 | href = ''; 21 | emoji = ''; 22 | } 23 | 24 | return `
25 | ${subnode.text || ''} ${emoji} 26 |
${subnodesToHTML(subnode.nodes, color)}
27 |
`; 28 | }).join(''); 29 | }; 30 | 31 | export default subnodesToHTML; 32 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import ViewUI from 'view-design'; 4 | import 'view-design/dist/styles/iview.css'; 5 | import VueClipboard from 'vue-clipboard2'; 6 | import VueMindmap from 'vue-mindmap'; 7 | import 'vue-mindmap/dist/vue-mindmap.css'; 8 | import Vue from 'vue'; 9 | import App from './App'; 10 | import router from './router'; 11 | import store from './vuex'; 12 | 13 | Vue.config.productionTip = false; 14 | Vue.use(ViewUI); 15 | Vue.use(VueMindmap); 16 | VueClipboard.config.autoSetContainer = true; // add this line 17 | Vue.use(VueClipboard); 18 | 19 | /* eslint-disable no-new */ 20 | new Vue({ 21 | el: '#app', 22 | router, 23 | store, 24 | components: { App }, 25 | template: '', 26 | }); 27 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | 4 | Vue.use(VueRouter); 5 | 6 | const routes = [ 7 | { 8 | path: '/', 9 | name: 'Welcome', 10 | component: () => import('../views/WelcomeCardView.vue'), 11 | meta: { 12 | nickName: '主页', 13 | level: 1, 14 | }, 15 | }, 16 | { 17 | path: '/login', 18 | name: 'Login', 19 | component: () => import('../views/UserLoginView.vue'), 20 | meta: { 21 | nickName: '登录', 22 | level: 2, 23 | }, 24 | }, 25 | { 26 | path: '/articleTable', 27 | name: 'ArticleTable', 28 | component: () => import(/* webpackChunkName: "about" */ '../views/ArticleTableView.vue'), 29 | meta: { 30 | nickName: '文献目录', 31 | name: ['文献目录'], 32 | path: ['/articleTable'], 33 | level: 2, 34 | }, 35 | }, 36 | { 37 | path: '/articleMde', 38 | name: 'ArticleMarkDownEditor', 39 | component: () => import('../views/MarkDownEditorView'), 40 | meta: { 41 | nickName: '文献笔记', 42 | name: ['文献目录', '文献笔记'], 43 | path: ['/articleTable', '/articleMde'], 44 | level: 10, 45 | }, 46 | }, 47 | { 48 | path: '/user_register', 49 | name: 'Register', 50 | component: () => import('../views/UserRegisterView'), 51 | meta: { 52 | nickName: '注册', 53 | level: 2, 54 | }, 55 | }, 56 | { 57 | path: '/userProfile', 58 | name: 'UserProfile', 59 | component: () => import('../views/UserProfileView'), 60 | meta: { 61 | nickName: '用户档案', 62 | level: 2, 63 | }, 64 | }, 65 | { 66 | path: '/editor', 67 | name: 'Editor', 68 | component: () => import(/* webpackChunkName: "about" */ '../views/RoadmapEditorView.vue'), 69 | meta: { 70 | nickName: '路书编辑', 71 | name: ['路书目录', '路书编辑页'], 72 | path: ['/RoadmapTable', '/editor'], 73 | level: 5, 74 | }, 75 | }, 76 | { 77 | path: '/essayTable', 78 | name: 'EssayTable', 79 | component: () => import(/* webpackChunkName: "about" */ '../views/EssayTableView.vue'), 80 | meta: { 81 | nickName: '随笔目录', 82 | name: ['随笔目录'], 83 | path: ['/essayTable'], 84 | level: 2, 85 | }, 86 | }, 87 | { 88 | path: '/RoadMapTable', 89 | name: 'RoadmapTable', 90 | component: () => import(/* webpackChunkName: "about" */ '../views/RoadmapTableView'), 91 | meta: { 92 | nickName: '路书目录', 93 | name: ['路书目录'], 94 | path: ['/RoadmapTable'], 95 | level: 2, 96 | }, 97 | }, 98 | { 99 | path: '/reader', 100 | name: 'Reader', 101 | component: () => import(/* webpackChunkName: "about" */ '../views/RoadmapReaderView.vue'), 102 | meta: { 103 | nickName: '路书阅览', 104 | name: ['路书目录', '路书阅览'], 105 | path: ['/RoadmapTable', '/reader'], 106 | level: 5, 107 | }, 108 | }, 109 | { 110 | path: '/essayReader', 111 | name: 'EssayReader', 112 | component: () => import(/* webpackChunkName: "about" */ '../views/EssayReaderView.vue'), 113 | meta: { 114 | nickName: '随笔阅览', 115 | name: ['随笔目录', '随笔阅览'], 116 | path: ['/essayTable', '/essayReader'], 117 | level: 5, 118 | }, 119 | }, 120 | { 121 | path: '/essayEditor', 122 | name: 'EssayEditor', 123 | component: () => import(/* webpackChunkName: "about" */ '../views/EssayEditor.vue'), 124 | meta: { 125 | nickName: '随笔编辑', 126 | name: ['随笔目录', '路书编辑'], 127 | path: ['/RoadmapTable', '/editor'], 128 | level: 5, 129 | }, 130 | }, 131 | { 132 | path: '/PaperRecommend', 133 | name: 'PaperRecommend', 134 | component: () => import('../components/PaperRecommend'), 135 | meta: { 136 | nickName: '文献推荐', 137 | level: 2, 138 | }, 139 | }, 140 | { 141 | path: '/essayRoadmapReader', 142 | name: 'EssayRoadmapReader', 143 | component: () => import(/* webpackChunkName: "about" */ '../views/EssayRoadmapBindReader.vue'), 144 | meta: { 145 | nickName: '随笔阅览', 146 | name: ['随笔目录', '随笔阅览'], 147 | path: ['/essayTable', '/essayReader'], 148 | level: 5, 149 | }, 150 | }, 151 | ]; 152 | 153 | const router = new VueRouter({ 154 | mode: 'history', 155 | base: process.env.BASE_URL, 156 | routes, 157 | }); 158 | 159 | export default router; 160 | -------------------------------------------------------------------------------- /src/views/ArticleTableView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /src/views/EssayEditor.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 184 | 185 | 186 | 193 | -------------------------------------------------------------------------------- /src/views/EssayReaderView.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 189 | 190 | 191 | 198 | -------------------------------------------------------------------------------- /src/views/EssayRoadmapBindReader.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 188 | 189 | 190 | 206 | -------------------------------------------------------------------------------- /src/views/EssayTableView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 47 | 48 | 51 | -------------------------------------------------------------------------------- /src/views/MarkDownEditorView.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 130 | 131 | 132 | 145 | -------------------------------------------------------------------------------- /src/views/RoadmapLayout.vue: -------------------------------------------------------------------------------- 1 | 92 | 156 | 157 | 193 | -------------------------------------------------------------------------------- /src/views/RoadmapTableView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/views/UserLoginView.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 79 | 80 | 93 | -------------------------------------------------------------------------------- /src/views/UserRegisterView.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 56 | 176 | 177 | 189 | -------------------------------------------------------------------------------- /src/views/WelcomeCardView.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | -------------------------------------------------------------------------------- /src/vuex/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import cookie from 'vue-cookies'; 4 | 5 | Vue.use(Vuex); 6 | 7 | /* 设置state参数 */ 8 | const state = { 9 | example: 'vuex is backing up', 10 | txtArr: ['hdlnb', 'hdlswlp'], 11 | authToken: cookie.get('authToken'), 12 | roadMapTable: cookie.get('roadMapTable'), 13 | }; 14 | 15 | const getters = { 16 | // eslint-disable-next-line no-shadow 17 | getFirstTxtArr(state) { 18 | if (state.txtArr.length !== 0) { return state.txtArr[0]; } 19 | return ''; 20 | }, 21 | }; 22 | 23 | const mutations = { 24 | // eslint-disable-next-line no-shadow 25 | pushTxtArr(state, str) { 26 | state.txtArr.push(str); 27 | }, 28 | popTxtArr() { 29 | state.txtArr.pop(); 30 | }, 31 | // eslint-disable-next-line no-shadow 32 | pushAuthToken(state, authToken) { 33 | state.authToken = authToken; 34 | cookie.set('authToken', authToken); 35 | }, 36 | // eslint-disable-next-line no-shadow 37 | pushRoadMapTable(state, style) { 38 | state.roadMapTable = style; 39 | cookie.set('roadMapTable', style); 40 | }, 41 | }; 42 | 43 | const actions = { 44 | 45 | }; 46 | 47 | const store = new Vuex.Store({ 48 | state, 49 | getters, 50 | mutations, 51 | actions, 52 | }); 53 | 54 | export default store; 55 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinJieDev/Roadmap-Frontend/e0643803244be193e5671bb8cc919d89915effd5/static/.gitkeep -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../../'), 5 | moduleFileExtensions: [ 6 | 'js', 7 | 'json', 8 | 'vue', 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1', 12 | }, 13 | transform: { 14 | '^.+\\.js$': '/node_modules/babel-jest', 15 | '.*\\.(vue)$': '/node_modules/vue-jest', 16 | }, 17 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 18 | setupFiles: ['/test/unit/setup'], 19 | mapCoverage: true, 20 | coverageDirectory: '/test/unit/coverage', 21 | collectCoverageFrom: [ 22 | 'src/**/*.{js,vue}', 23 | '!src/main.js', 24 | '!src/router/index.js', 25 | '!**/node_modules/**', 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | Vue.config.productionTip = false; 4 | -------------------------------------------------------------------------------- /test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import HelloWorld from '@/components/HelloWorld'; 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld); 7 | const vm = new Constructor().$mount(); 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .toEqual('Welcome to Your Vue.js App'); 10 | }); 11 | }); 12 | --------------------------------------------------------------------------------