├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── build ├── setup-dev-server.js ├── webpack.base.config.js ├── webpack.client.config.js └── webpack.server.config.js ├── manifest.json ├── package-lock.json ├── package.json ├── public ├── 11.jpg ├── 12.jpg ├── demo1.png ├── demo2.png ├── demo3.png ├── favicon.png ├── logo-144.png └── logo-48.png ├── server.js ├── server ├── canvas-echart.js └── server-data.js ├── src ├── App.vue ├── app.js ├── assets │ ├── adminContent.scss │ ├── dataAnalysis.scss │ ├── el-table.scss │ ├── head.scss │ ├── img-path.scss │ ├── index.scss │ ├── leftNavigation.scss │ ├── manage.scss │ ├── menuContainer.scss │ └── mixins │ │ ├── _admin-content.scss │ │ ├── _admin-head.scss │ │ ├── _admin-left.scss │ │ └── _admin-top.scss ├── components │ └── ProgressBar.vue ├── entry-client.js ├── entry-server.js ├── img │ ├── key.png │ ├── logo-home.png │ ├── logo-system.png │ ├── logo-work.png │ ├── logo.png │ └── logout.png ├── index.template.html ├── router │ └── index.js ├── store │ └── index.js ├── util │ ├── filters.js │ └── title.js └── views │ ├── adminContent.vue │ ├── dataAnalysis.vue │ ├── header.vue │ ├── leftNavigation.vue │ ├── menuContainer.vue │ ├── movieManage.vue │ └── userManage.vue └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | //{ 2 | // "presets": [ 3 | // ["env", { "modules": false }] 4 | // ], 5 | // "plugins": [ 6 | // "syntax-dynamic-import" 7 | // ] 8 | //} 9 | { 10 | "presets": ["es2015", "stage-2"], 11 | "plugins": [ 12 | "syntax-dynamic-import" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | .idea 7 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-present, Yuxi (Evan) You 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-ssr 2 | 3 | This instance built with Vue 2.0 + vue-router + vuex + element-ui + echart.js, with server-side rendering. 4 | 5 |

6 | 7 | 8 | 9 |

10 | 11 | ## 特点 12 | 13 | > Note: 这个项目是在尤大大给出的官方demo实例 HackerNews Demo 上进行改造的,克服尤大大给的 HackerNews Demo 需要翻墙才能运行起来的问题,新手在阅读SSR官方文档时,如果遇到疑惑点,可以直接在本文实例的基础上进行相关实验验证,从而解决疑惑,帮助国内ssr初学者更容易入门;并且本实例使用最受欢迎的vue ui库element-ui组件库和可视化echarts插件,演示如何在ssr中使用ui库以及进行数据可视化等。 14 | 15 | ## SSR 结构示意图 16 | 17 | screen shot 2016-08-11 at 6 06 57 pm 18 | 19 | **vue ssr 官方文档见连接 [here](https://ssr.vuejs.org).** 20 | 21 | ## 构建步骤 22 | 23 | **需要 Node.js 7+ (作者使用的版本为:node.js 8.2.1 npm 5.3.0)** 24 | 25 | ``` bash 26 | # install dependencies 27 | npm install 28 | 29 | # serve in dev mode, with hot reload at localhost:80 30 | npm run dev 31 | 32 | # build for production 33 | npm run build 34 | 35 | # serve in production mode 36 | npm start 37 | ``` 38 | 39 | ## License 40 | 41 | MIT 42 | -------------------------------------------------------------------------------- /build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const MFS = require('memory-fs') 4 | const webpack = require('webpack') 5 | const chokidar = require('chokidar') 6 | const clientConfig = require('./webpack.client.config') 7 | const serverConfig = require('./webpack.server.config') 8 | 9 | const readFile = (fs, file) => { 10 | try { 11 | return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') 12 | } catch (e) {} 13 | } 14 | 15 | module.exports = function setupDevServer (app, templatePath, cb) { 16 | let bundle 17 | let template 18 | let clientManifest 19 | 20 | let ready 21 | const readyPromise = new Promise(r => { ready = r }) 22 | const update = () => { 23 | if (bundle && clientManifest) { 24 | ready() 25 | cb(bundle, { 26 | template, 27 | clientManifest 28 | }) 29 | } 30 | } 31 | 32 | // read template from disk and watch 33 | template = fs.readFileSync(templatePath, 'utf-8') 34 | chokidar.watch(templatePath).on('change', () => { 35 | template = fs.readFileSync(templatePath, 'utf-8') 36 | console.log('index.html template updated.') 37 | update() 38 | }) 39 | 40 | // modify client config to work with hot middleware 41 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 42 | clientConfig.output.filename = '[name].js' 43 | clientConfig.plugins.push( 44 | new webpack.HotModuleReplacementPlugin(), 45 | new webpack.NoEmitOnErrorsPlugin() 46 | ) 47 | 48 | // dev middleware 49 | const clientCompiler = webpack(clientConfig) 50 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 51 | publicPath: clientConfig.output.publicPath, 52 | noInfo: true 53 | }) 54 | app.use(devMiddleware) 55 | clientCompiler.plugin('done', stats => { 56 | stats = stats.toJson() 57 | stats.errors.forEach(err => console.error(err)) 58 | stats.warnings.forEach(err => console.warn(err)) 59 | if (stats.errors.length) return 60 | clientManifest = JSON.parse(readFile( 61 | devMiddleware.fileSystem, 62 | 'vue-ssr-client-manifest.json' 63 | )) 64 | update() 65 | }) 66 | 67 | // hot middleware 68 | app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 })) 69 | 70 | // watch and update server renderer 71 | const serverCompiler = webpack(serverConfig) 72 | const mfs = new MFS() 73 | serverCompiler.outputFileSystem = mfs 74 | serverCompiler.watch({}, (err, stats) => { 75 | if (err) throw err 76 | stats = stats.toJson() 77 | if (stats.errors.length) return 78 | 79 | // read bundle generated by vue-ssr-webpack-plugin 80 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 81 | update() 82 | }) 83 | 84 | return readyPromise 85 | } 86 | -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 5 | const { VueLoaderPlugin } = require('vue-loader') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | module.exports = { 10 | devtool: isProd 11 | ? false 12 | : '#cheap-module-source-map', 13 | output: { 14 | path: path.resolve(__dirname, '../dist'), 15 | publicPath: '/dist/', 16 | filename: '[name].[chunkhash].js' 17 | }, 18 | resolve: { 19 | alias: { 20 | 'public': path.resolve(__dirname, '../public') 21 | } 22 | }, 23 | module: { 24 | noParse: /es6-promise\.js$/, // avoid webpack shimming process 25 | rules: [ 26 | { 27 | test: /\.vue$/, 28 | loader: 'vue-loader', 29 | options: { 30 | compilerOptions: { 31 | preserveWhitespace: false 32 | } 33 | } 34 | }, 35 | { 36 | test: /\.js$/, 37 | loader: 'babel-loader', 38 | exclude: /node_modules/ 39 | }, 40 | { 41 | test: /\.(png|jpg|gif|svg)$/, 42 | loader: 'url-loader', 43 | options: { 44 | limit: 10000, 45 | name: '[name].[ext]?[hash]' 46 | } 47 | }, 48 | { //下面这段是vue2.0需要的scss配置 49 | test: /\.scss$/, 50 | loaders: ["vue-style-loader","css-loader", "sass-loader"] 51 | }, 52 | { 53 | test: /\.css$/, 54 | loader: ["vue-style-loader", "css-loader"] 55 | }, 56 | { 57 | test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/, 58 | loader: 'file-loader' 59 | }, 60 | { 61 | test: /\.styl(us)?$/, 62 | use: isProd 63 | ? ExtractTextPlugin.extract({ 64 | use: [ 65 | { 66 | loader: 'css-loader', 67 | options: { minimize: true } 68 | }, 69 | 'stylus-loader' 70 | ], 71 | fallback: 'vue-style-loader' 72 | }) 73 | : ['vue-style-loader', 'css-loader', 'stylus-loader'] 74 | }, 75 | ] 76 | }, 77 | performance: { 78 | hints: false 79 | }, 80 | plugins: isProd 81 | ? [ 82 | new VueLoaderPlugin(), 83 | new webpack.optimize.UglifyJsPlugin({ 84 | compress: { warnings: false } 85 | }), 86 | new webpack.optimize.ModuleConcatenationPlugin(), 87 | new ExtractTextPlugin({ 88 | filename: 'common.[chunkhash].css' 89 | }) 90 | ] 91 | : [ 92 | new VueLoaderPlugin(), 93 | new FriendlyErrorsPlugin() 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const SWPrecachePlugin = require('sw-precache-webpack-plugin') 5 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 6 | 7 | const config = merge(base, { 8 | entry: { 9 | app: './src/entry-client.js' 10 | }, 11 | resolve: { 12 | alias: { 13 | } 14 | }, 15 | plugins: [ 16 | // strip dev-only code in Vue source 17 | new webpack.DefinePlugin({ 18 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 19 | 'process.env.VUE_ENV': '"client"' 20 | }), 21 | // extract vendor chunks for better caching 22 | new webpack.optimize.CommonsChunkPlugin({ 23 | name: 'vendor', 24 | minChunks: function (module) { 25 | // a module is extracted into the vendor chunk if... 26 | return ( 27 | // it's inside node_modules 28 | /node_modules/.test(module.context) && 29 | // and not a CSS file (due to extract-text-webpack-plugin limitation) 30 | !/\.css$/.test(module.request) 31 | ) 32 | } 33 | }), 34 | // extract webpack runtime & manifest to avoid vendor chunk hash changing 35 | // on every build. 36 | new webpack.optimize.CommonsChunkPlugin({ 37 | name: 'manifest' 38 | }), 39 | new VueSSRClientPlugin() 40 | ] 41 | }) 42 | 43 | if (process.env.NODE_ENV === 'production') { 44 | config.plugins.push( 45 | // auto generate service worker 46 | new SWPrecachePlugin({ 47 | cacheId: 'vue-hn', 48 | filename: 'service-worker.js', 49 | minify: true, 50 | dontCacheBustUrlsMatching: /./, 51 | staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/], 52 | runtimeCaching: [ 53 | { 54 | urlPattern: '/', 55 | handler: 'networkFirst' 56 | }, 57 | { 58 | urlPattern: /\/(top|new|show|ask|jobs)/, 59 | handler: 'networkFirst' 60 | }, 61 | { 62 | urlPattern: '/item/:id', 63 | handler: 'networkFirst' 64 | }, 65 | { 66 | urlPattern: '/user/:id', 67 | handler: 'networkFirst' 68 | } 69 | ] 70 | }) 71 | ) 72 | } 73 | 74 | module.exports = config 75 | -------------------------------------------------------------------------------- /build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const nodeExternals = require('webpack-node-externals') 5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 6 | 7 | module.exports = merge(base, { 8 | // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), 9 | // 并且还会在编译 Vue 组件时, 10 | // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 11 | target: 'node', 12 | // 对 bundle renderer 提供 source map 支持 13 | devtool: '#source-map', 14 | // 将entry 指向应用程序的 server entry 文件 15 | entry: './src/entry-server.js', 16 | // 导出配置 17 | output: { 18 | // 导出名称 19 | filename: 'server-bundle.js', 20 | // 服务端使用common.js 规范 21 | libraryTarget: 'commonjs2' 22 | }, 23 | resolve: { 24 | alias: { 25 | } 26 | }, 27 | // https://webpack.js.org/configuration/externals/#externals 28 | // https://github.com/liady/webpack-node-externals 29 | // 外置化应用程序依赖模块。可以使服务器构建速度更快, 30 | // 并生成较小的 bundle 文件。 31 | externals: nodeExternals({ 32 | // css 还应该由 webpack 处理 33 | whitelist: /\.css$/ 34 | }), 35 | 36 | // 这是将服务器的整个输出 37 | // 构建为单个 JSON 文件的插件。 38 | // 默认文件名为 `vue-ssr-server-bundle.json` 39 | plugins: [ 40 | new webpack.DefinePlugin({ 41 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 42 | 'process.env.VUE_ENV': '"server"' 43 | }), 44 | new VueSSRServerPlugin() 45 | ] 46 | }) 47 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue Hackernews 2.0", 3 | "short_name": "Vue HN", 4 | "icons": [{ 5 | "src": "/public/logo-120.png", 6 | "sizes": "120x120", 7 | "type": "image/png" 8 | }, { 9 | "src": "/public/logo-144.png", 10 | "sizes": "144x144", 11 | "type": "image/png" 12 | }, { 13 | "src": "/public/logo-152.png", 14 | "sizes": "152x152", 15 | "type": "image/png" 16 | }, { 17 | "src": "/public/logo-192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }, { 21 | "src": "/public/logo-256.png", 22 | "sizes": "256x256", 23 | "type": "image/png" 24 | }, { 25 | "src": "/public/logo-384.png", 26 | "sizes": "384x384", 27 | "type": "image/png" 28 | }, { 29 | "src": "/public/logo-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png" 32 | }], 33 | "start_url": "/", 34 | "background_color": "#f2f3f5", 35 | "display": "standalone", 36 | "theme_color": "#f60" 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-hackernews-2.0", 3 | "description": "A Vue.js project", 4 | "author": "Evan You ", 5 | "private": true, 6 | "scripts": { 7 | "dev": "node server", 8 | "start": "cross-env NODE_ENV=production node server", 9 | "build": "rimraf dist && npm run build:client && npm run build:server", 10 | "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules", 11 | "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules", 12 | "postinstall": "npm run build" 13 | }, 14 | "engines": { 15 | "node": ">=7.0", 16 | "npm": ">=4.0" 17 | }, 18 | "dependencies": { 19 | "axios": "^0.16.0", 20 | "canvas": "^2.4.1", 21 | "compression": "^1.7.1", 22 | "cross-env": "^5.1.1", 23 | "element-ui": "^2.7.2", 24 | "es6-promise": "^4.1.1", 25 | "express": "^4.16.2", 26 | "extract-text-webpack-plugin": "^3.0.2", 27 | "firebase": "4.6.2", 28 | "lru-cache": "^4.1.1", 29 | "route-cache": "0.4.3", 30 | "serve-favicon": "^2.4.5", 31 | "style-loader": "^0.23.1", 32 | "vue": "^2.5.22", 33 | "vue-router": "^3.0.1", 34 | "vue-server-renderer": "^2.5.22", 35 | "vuex": "^3.0.1", 36 | "vuex-router-sync": "^5.0.0" 37 | }, 38 | "devDependencies": { 39 | "autoprefixer": "^7.1.6", 40 | "babel-core": "^6.26.0", 41 | "babel-loader": "^7.1.2", 42 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 43 | "babel-preset-env": "^1.6.1", 44 | "babel-preset-es2015": "^6.24.1", 45 | "babel-preset-stage-2": "^6.24.1", 46 | "chokidar": "^1.7.0", 47 | "css-loader": "^0.28.7", 48 | "file-loader": "^1.1.5", 49 | "friendly-errors-webpack-plugin": "^1.6.1", 50 | "node-echarts": "^1.1.4", 51 | "node-sass": "^4.11.0", 52 | "rimraf": "^2.6.2", 53 | "sass-loader": "^7.1.0", 54 | "stylus": "^0.54.5", 55 | "stylus-loader": "^3.0.1", 56 | "sw-precache-webpack-plugin": "^0.11.4", 57 | "url-loader": "^0.6.2", 58 | "vue-loader": "^15.3.0", 59 | "vue-style-loader": "^4.1.2", 60 | "vue-template-compiler": "^2.5.22", 61 | "webpack": "^3.8.1", 62 | "webpack-dev-middleware": "^1.12.0", 63 | "webpack-hot-middleware": "^2.20.0", 64 | "webpack-merge": "^4.2.1", 65 | "webpack-node-externals": "^1.7.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/public/11.jpg -------------------------------------------------------------------------------- /public/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/public/12.jpg -------------------------------------------------------------------------------- /public/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/public/demo1.png -------------------------------------------------------------------------------- /public/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/public/demo2.png -------------------------------------------------------------------------------- /public/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/public/demo3.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/public/favicon.png -------------------------------------------------------------------------------- /public/logo-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/public/logo-144.png -------------------------------------------------------------------------------- /public/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/public/logo-48.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const LRU = require('lru-cache') 4 | const express = require('express') 5 | const favicon = require('serve-favicon') 6 | const compression = require('compression') 7 | const microcache = require('route-cache') 8 | const resolve = file => path.resolve(__dirname, file) 9 | const { createBundleRenderer } = require('vue-server-renderer') 10 | const { generateImage } = require('./server/canvas-echart.js') 11 | var serverData = require('./server/server-data.js'); 12 | 13 | // process.env.NODE_ENV 为 undefined 14 | const isProd = process.env.NODE_ENV === 'production' 15 | const useMicroCache = process.env.MICRO_CACHE !== 'false' 16 | const serverInfo = 17 | `express/${require('express/package.json').version} ` + 18 | `vue-server-renderer/${require('vue-server-renderer/package.json').version}` 19 | 20 | const app = express() 21 | 22 | // 服务端渲染相关配置 23 | function createRenderer (bundle, options) { 24 | // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer 25 | return createBundleRenderer(bundle, Object.assign(options, { 26 | // 组件级别的缓存(不起作用?) 27 | cache: LRU({ 28 | max: 1000 * 60 * 15, 29 | maxAge: 1000 * 60 * 15 30 | }), 31 | // 把路径解析为绝对路径 32 | basedir: resolve('./dist'), 33 | // runInNewContext: true(默认) 对于每次渲染,bundle renderer 将创建一个新的 V8 上下文并重新执行整个 bundle 34 | // 优点:无需担心状态单例问题;缺点:性能开销大 35 | runInNewContext: false 36 | })) 37 | } 38 | 39 | let renderer 40 | let readyPromise 41 | const templatePath = resolve('./src/index.template.html') 42 | if (isProd) { 43 | // 读取html模板 44 | const template = fs.readFileSync(templatePath, 'utf-8') 45 | // bundle 为服务端渲染入口 46 | const bundle = require('./dist/vue-ssr-server-bundle.json') 47 | // clientManifest 为客户端渲染入口 48 | const clientManifest = require('./dist/vue-ssr-client-manifest.json') 49 | renderer = createRenderer(bundle, { 50 | template, 51 | clientManifest 52 | }) 53 | } else { 54 | // 开发环境:使用 setup-dev-server.js 并且有监听和热重载功能 55 | readyPromise = require('./build/setup-dev-server')( 56 | app, 57 | templatePath, 58 | (bundle, options) => { 59 | renderer = createRenderer(bundle, options) 60 | } 61 | ) 62 | } 63 | 64 | const serve = (path, cache) => express.static(resolve(path), { 65 | maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0 66 | }) 67 | 68 | app.use(compression({ threshold: 0 })) 69 | app.use(favicon('./public/favicon.png')) 70 | app.use('/dist', serve('./dist', true)) 71 | app.use('/public', serve('./public', true)) 72 | app.use('/manifest.json', serve('./manifest.json', true)) 73 | app.use('/service-worker.js', serve('./dist/service-worker.js')) 74 | 75 | // 页面级缓存,因为这个例子,所有用户访问的页面是一致的,所以开启缓存 76 | app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl)) 77 | 78 | 79 | // 前端请求的接口 api 80 | app.get('/api/getUserlist', (req, res) => { 81 | console.log('/api/getUserlist调用...'); 82 | res.json(serverData.userlist); 83 | }); 84 | 85 | // 前端请求的接口 api 86 | app.get('/api/getMovielist', (req, res) => { 87 | console.log('/api/getMovielist调用...'); 88 | res.json(serverData.movielist) 89 | }); 90 | 91 | // 前端请求的接口 api 92 | app.get('/api/getAnalysis', (req, res) => { 93 | console.log('/api/getAnalysis调用...'); 94 | var imgpath1 = '/public/11.jpg'; 95 | generateImage(serverData.option1,__dirname+imgpath1); 96 | 97 | var imgpath2 = '/public/12.jpg'; 98 | generateImage(serverData.option2,__dirname+imgpath2); 99 | res.json([imgpath1, imgpath2]) 100 | }); 101 | 102 | // 头部设置,以及页面返回逻辑处理:正常、404、500等 103 | function render (req, res) { 104 | const s = Date.now() 105 | 106 | res.setHeader("Content-Type", "text/html") 107 | res.setHeader("Server", serverInfo) 108 | 109 | const handleError = err => { 110 | if (err.url) { 111 | res.redirect(err.url) 112 | } else if(err.code === 404) { 113 | res.status(404).send('404 | Page Not Found') 114 | } else { 115 | // Render Error Page or Redirect 116 | res.status(500).send('500 | Internal Server Error') 117 | console.error(`error during render : ${req.url}`) 118 | console.error(err.stack) 119 | } 120 | } 121 | // 模板插值显示数据,显示在 index.template.html 模板中 122 | const context = { 123 | title: 'Movie', 124 | url: req.url 125 | }; 126 | renderer.renderToString(context, (err, html) => { 127 | if (err) { 128 | return handleError(err) 129 | } 130 | res.send(html) 131 | if (!isProd) { 132 | console.log(`whole request: ${Date.now() - s}ms`) 133 | } 134 | }) 135 | } 136 | 137 | app.get('*', isProd ? render : (req, res) => { 138 | readyPromise.then(() => render(req, res)) 139 | }) 140 | 141 | const port = process.env.PORT || 80 142 | app.listen(port, () => { 143 | console.log(`server started at localhost:${port}`) 144 | }) 145 | -------------------------------------------------------------------------------- /server/canvas-echart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | // const CanvasEchart = require('canvas'); 5 | const { createCanvas, loadImage } = require('canvas'); 6 | const echarts = require('echarts'); 7 | const fs = require('fs'); 8 | 9 | module.exports = { 10 | generateImage (options, savePath, size) { 11 | return new Promise((resolve, reject) => { 12 | const canvas = createCanvas(400, 200); 13 | const ctx = canvas.getContext('2d'); 14 | ctx.font = '12px 楷体'; 15 | echarts.setCanvasCreator(function () { 16 | return canvas; 17 | }); 18 | const chart = echarts.init(canvas); 19 | options.animation = false; 20 | options.textStyle = { 21 | fontFamily: '楷体', 22 | fontSize: 12, 23 | }; 24 | chart.setOption(options); 25 | try { 26 | fs.writeFileSync(savePath, chart.getDom().toBuffer()); 27 | console.log("Create Img:" + savePath); 28 | } catch (err){ 29 | console.error("Error: Write File failed" + err.message); 30 | } 31 | resolve(); 32 | }) 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /server/server-data.js: -------------------------------------------------------------------------------- 1 | module.exports.userlist = { 2 | userlist: [{ 3 | 'id': '1001', 4 | 'name': '张三', 5 | 'age': 15, 6 | 'sex': 0, 7 | 'role': '学生' 8 | }, 9 | { 10 | 'id': '1002', 11 | 'name': '李四', 12 | 'age': 25, 13 | 'sex': 0, 14 | 'role': '软件工程师' 15 | }, 16 | { 17 | 'id': '1003', 18 | 'name': '王五', 19 | 'age': 35, 20 | 'sex': 0, 21 | 'role': '老师' 22 | }, 23 | ] 24 | }; 25 | 26 | module.exports.movielist = { 27 | movielist: [{ 28 | 'id':'2001', 29 | 'name':'小鬼当家1', 30 | 'date':'2017-01-01', 31 | 'type':'喜剧', 32 | }, 33 | { 34 | 'id':'2002', 35 | 'name':'小鬼当家2', 36 | 'date':'2018-02-02', 37 | 'type':'动作', 38 | }, 39 | { 40 | 'id':'2003', 41 | 'name':'小鬼当家3', 42 | 'date':'2019-03-03', 43 | 'type':'枪战', 44 | }, 45 | ] 46 | }; 47 | 48 | module.exports.option1 = { 49 | title: { 50 | text: '电影分类统计' 51 | }, 52 | tooltip: {}, 53 | legend: { 54 | data:['数量'] 55 | }, 56 | xAxis: { 57 | data: ['喜剧','动作','战争','爱情','记录','恐怖'] 58 | }, 59 | yAxis: {}, 60 | series: [{ 61 | name: '数量', 62 | type: 'bar', 63 | data: [5, 20, 36, 10, 10, 20] 64 | }] 65 | }; 66 | 67 | module.exports.option2 = { 68 | title : { 69 | text: '电影分类统计', 70 | subtext: '纯属虚构', 71 | x:'center' 72 | }, 73 | tooltip : { 74 | trigger: 'item', 75 | formatter: "{a}
{b} : {c} ({d}%)" 76 | }, 77 | legend: { 78 | orient: 'vertical', 79 | left: 'left', 80 | data: ['喜剧','动作','战争','爱情','记录','恐怖'] 81 | }, 82 | series : [ 83 | { 84 | name: '访问来源', 85 | type: 'pie', 86 | radius : '55%', 87 | center: ['50%', '60%'], 88 | data:[ 89 | {value:5, name:'喜剧'}, 90 | {value:20, name:'动作'}, 91 | {value:36, name:'战争'}, 92 | {value:10, name:'爱情'}, 93 | {value:10, name:'记录'}, 94 | {value:20, name:'恐怖'}, 95 | ], 96 | itemStyle: { 97 | emphasis: { 98 | shadowBlur: 10, 99 | shadowOffsetX: 0, 100 | shadowColor: 'rgba(0, 0, 0, 0.5)' 101 | } 102 | } 103 | } 104 | ] 105 | }; 106 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 25 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import { createStore } from './store' 4 | import { createRouter } from './router' 5 | import { sync } from 'vuex-router-sync' 6 | import titleMixin from './util/title' 7 | import * as filters from './util/filters' 8 | import 'element-ui/lib/theme-chalk/index.css'; 9 | 10 | import { 11 | Button, 12 | Menu, 13 | Submenu, 14 | MenuItem, 15 | MenuItemGroup, 16 | Dialog, 17 | Table, 18 | TableColumn, 19 | MessageBox, 20 | Message, 21 | Pagination, 22 | Loading, 23 | } from 'element-ui'; 24 | 25 | Vue.use(Button); 26 | Vue.use(Menu); 27 | Vue.use(Submenu); 28 | Vue.use(MenuItem); 29 | Vue.use(MenuItemGroup); 30 | Vue.use(Dialog); 31 | Vue.use(Table); 32 | Vue.use(TableColumn); 33 | Vue.use(Pagination); 34 | 35 | Vue.use(Loading.directive); 36 | 37 | Vue.prototype.$alert = MessageBox.alert; 38 | Vue.prototype.$confirm = MessageBox.confirm; 39 | Vue.prototype.$message = Message; 40 | 41 | // mixin for handling title 42 | Vue.mixin(titleMixin) 43 | 44 | // register global utility filters. 45 | Object.keys(filters).forEach(key => { 46 | Vue.filter(key, filters[key]) 47 | }) 48 | 49 | // 导出一个工厂函数,用于创建新的根实例:利用一个可重复执行的工厂函数,为每个请求创建新的应用程序实例,避免导致状态污染 50 | // 同样的规则使用于router、store 和 event bus 实例 51 | export function createApp () { 52 | // 创建 store 和 router 实例 53 | const store = createStore() 54 | const router = createRouter() 55 | 56 | // 同步路由状态(route state)到 store 57 | sync(store, router) 58 | 59 | 60 | // 创建应用程序实例,将 router 和 store 注入 61 | const app = new Vue({ 62 | router, 63 | store, 64 | render: h => h(App) 65 | }) 66 | 67 | return { app, router, store } 68 | } 69 | -------------------------------------------------------------------------------- /src/assets/adminContent.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @import "./mixins/_admin-content.scss"; 4 | 5 | @include admin-content(admin-content); 6 | -------------------------------------------------------------------------------- /src/assets/dataAnalysis.scss: -------------------------------------------------------------------------------- 1 | @import "./mixins/_admin-top.scss"; 2 | @include admin-top(admin-usermg); 3 | .analysis { 4 | &__layout { 5 | display: flex; 6 | justify-content: space-around; 7 | margin-top: 30px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/el-table.scss: -------------------------------------------------------------------------------- 1 | .el-table__header{ 2 | width: 100%; 3 | } 4 | .el-table__body{ 5 | width: 100%; 6 | } 7 | .el-table-column--selection{ 8 | width: 48px; 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/head.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @import "./mixins/_admin-head.scss"; 4 | 5 | @include admin-head(admin-head); 6 | 7 | .admin-head { 8 | &__setting { 9 | float: right; 10 | padding: 0 20px 0 10px; 11 | border-left: 1px solid #e8e8e8; 12 | cursor: pointer; 13 | 14 | & > a { 15 | display: block; 16 | color: #666; 17 | } 18 | } 19 | 20 | &__img { 21 | margin: 15px 5px 0 0; 22 | float: left; 23 | } 24 | 25 | &__password { 26 | cursor: pointer; 27 | float: right; 28 | color: #666; 29 | padding: 0 20px 0 10px; 30 | border-left: 1px solid #e8e8e8; 31 | } 32 | 33 | &__wel{ 34 | float: right; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/img-path.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | $common-path: '../img/'; 4 | 5 | /* 管理端 */ 6 | $logo-home: $common-path+'logo-home.png'; 7 | $logo-system: $common-path+'logo-system.png'; 8 | $logo-work: $common-path+'logo-work.png'; 9 | -------------------------------------------------------------------------------- /src/assets/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | /* 公共 */ 4 | @import 'img-path.scss'; 5 | 6 | /* 组件 */ 7 | //@import "menuContainer.scss"; 8 | 9 | /* 总页面布局 */ 10 | //@import "head.scss"; 11 | //@import "adminContent.scss"; 12 | //@import "leftNavigation.scss"; 13 | //@import "classManage.scss"; 14 | -------------------------------------------------------------------------------- /src/assets/leftNavigation.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @import "./mixins/_admin-left.scss"; 4 | 5 | @include admin-left(admin-left); 6 | .left{ 7 | &__font{ 8 | color: #ccc; 9 | font-size: 14px; 10 | vertical-align: super; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/manage.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @import "./mixins/_admin-top.scss"; 4 | 5 | @include admin-top(admin-usermg); 6 | 7 | -------------------------------------------------------------------------------- /src/assets/menuContainer.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @mixin menu-icon($logo-img) { 4 | display: inline-block; 5 | height: 24px; 6 | width: 24px; 7 | background: url($logo-img) no-repeat; 8 | } 9 | 10 | .com-menuContainer { 11 | &__btn-account-manage { 12 | @include menu-icon($logo-home); 13 | } 14 | 15 | &__btn-homework-manage { 16 | @include menu-icon($logo-work); 17 | } 18 | 19 | &__btn-system-setting { 20 | @include menu-icon($logo-system); 21 | } 22 | 23 | &__submenu-icon { 24 | float: left !important; 25 | height: 30px !important; 26 | margin-top: 2px !important; 27 | list-style: circle inside !important; 28 | padding-left: 20px !important; 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/assets/mixins/_admin-content.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @mixin admin-content($class) { 4 | .#{$class} { 5 | &__top { 6 | padding-top: 50px; 7 | margin-left: 200px; 8 | z-index: 70; 9 | min-width: 760px; 10 | } 11 | 12 | &__middle { 13 | position: relative; 14 | height: 100%; 15 | width: 100%; 16 | background-color: #ebf0f5; 17 | } 18 | 19 | &__inside { 20 | height: 100%; 21 | padding-left: 10px; 22 | padding-right: 10px; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/mixins/_admin-head.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @mixin admin-head($class) { 4 | .#{$class} { 5 | &__top { 6 | position: fixed; 7 | height: 50px; 8 | line-height: 50px; 9 | background: #fff; 10 | width: 100%; 11 | color: #666; 12 | font-size: 12px; 13 | z-index: 99; 14 | } 15 | 16 | &__wel { 17 | float: right; 18 | padding: 0 20px 0 10px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/assets/mixins/_admin-left.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @mixin admin-left($class) { 4 | .#{$class} { 5 | position: fixed; 6 | width: 200px; 7 | height: 100%; 8 | background-color: #162238; 9 | z-index: 99; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/mixins/_admin-top.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @mixin admin-top($class) { 4 | .#{$class} { 5 | font-size: 12px; 6 | color: #333; 7 | 8 | 9 | &__top { 10 | height: 50px; 11 | line-height: 50px; 12 | padding-left: 10px; 13 | font-size: 18px; 14 | color: #000; 15 | border-bottom: 1px solid #b8cee0; 16 | } 17 | 18 | &__button { 19 | margin: 5px; 20 | .adminPage{ 21 | display:inline-block; 22 | float:right; 23 | margin: -6px; 24 | } 25 | } 26 | 27 | &__pagi { 28 | float: right; 29 | } 30 | 31 | &__expand { 32 | border: 0 !important; 33 | } 34 | } 35 | 36 | .el-pagination__editor { 37 | margin-top: 0 !important; 38 | height: 28px; 39 | width: 28px; 40 | } 41 | } 42 | 43 | .el-table { 44 | font-size: 12px !important; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 88 | 89 | 102 | -------------------------------------------------------------------------------- /src/entry-client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import 'es6-promise/auto' 3 | import { createApp } from './app' 4 | import ProgressBar from './components/ProgressBar.vue' 5 | 6 | // 路由切换时,页面数据加载指示器 7 | const bar = Vue.prototype.$bar = new Vue(ProgressBar).$mount() 8 | document.body.appendChild(bar.$el) 9 | 10 | // a global mixin that calls `asyncData` when a route component's params change 11 | Vue.mixin({ 12 | beforeRouteUpdate (to, from, next) { 13 | const { asyncData } = this.$options 14 | if (asyncData) { 15 | asyncData({ 16 | store: this.$store, 17 | route: to 18 | }).then(next).catch(next) 19 | } else { 20 | next() 21 | } 22 | } 23 | }) 24 | 25 | const { app, router, store } = createApp() 26 | 27 | // 将服务端渲染的vuex数据,替换到客户端的vuex中 28 | if (window.__INITIAL_STATE__) { 29 | store.replaceState(window.__INITIAL_STATE__) 30 | } 31 | 32 | // 路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子 33 | // 路由准备就绪时执行一次 34 | router.onReady(() => { 35 | 36 | // 注册钩子,客户端每次路由切换都会执行一次 37 | router.beforeResolve((to, from, next) => { 38 | const matched = router.getMatchedComponents(to) 39 | const prevMatched = router.getMatchedComponents(from) 40 | 41 | // 我们只关心非预渲染的组件 42 | // 所以我们对比它们,找出两个匹配列表的差异组件 43 | let diffed = false 44 | const activated = matched.filter((c, i) => { 45 | return diffed || (diffed = (prevMatched[i] !== c)) 46 | }) 47 | const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) 48 | if (!asyncDataHooks.length) { 49 | return next() 50 | } 51 | // 触发加载指示器 52 | bar.start(); 53 | Promise.all(asyncDataHooks.map(hook => hook({ store, route: to }))) 54 | .then(() => { 55 | // 停止加载指示器 56 | bar.finish() 57 | next() 58 | }) 59 | .catch(next) 60 | }) 61 | 62 | // 挂载到DOM 中 63 | app.$mount('#app') 64 | }) 65 | 66 | // service worker 67 | if ('https:' === location.protocol && navigator.serviceWorker) { 68 | navigator.serviceWorker.register('/service-worker.js') 69 | } 70 | -------------------------------------------------------------------------------- /src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | const isDev = process.env.NODE_ENV !== 'production' 4 | 5 | // 因为有可能会是异步路由钩子函数或组件,所以将返回一个 Promise, 6 | // 以便服务器能够等待所有的内容在渲染前,就已经准备就绪。 7 | export default context => { 8 | return new Promise((resolve, reject) => { 9 | const s = isDev && Date.now() 10 | const { app, router, store } = createApp() 11 | 12 | const { url } = context 13 | const { fullPath } = router.resolve(url).route 14 | 15 | if (fullPath !== url) { 16 | return reject({ url: fullPath }) 17 | } 18 | 19 | // 设置服务器端 router 的位置 20 | router.push(url) 21 | 22 | // 等到 router 将可能的异步组件和钩子函数解析完 23 | router.onReady(() => { 24 | const matchedComponents = router.getMatchedComponents() 25 | // no matched routes 26 | if (!matchedComponents.length) { 27 | return reject({ code: 404 }) 28 | } 29 | 30 | // 对所有匹配的路由组件调用 `asyncData()` 31 | Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ 32 | store, 33 | route: router.currentRoute 34 | }))).then(() => { 35 | isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) 36 | 37 | // 在所有预取钩子(preFetch hook) resolve 后, 38 | // 我们的 store 现在已经填充入渲染应用程序所需的状态。 39 | // 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中 40 | context.state = store.state 41 | resolve(app) 42 | }).catch(reject) 43 | }, reject) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/img/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/src/img/key.png -------------------------------------------------------------------------------- /src/img/logo-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/src/img/logo-home.png -------------------------------------------------------------------------------- /src/img/logo-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/src/img/logo-system.png -------------------------------------------------------------------------------- /src/img/logo-work.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/src/img/logo-work.png -------------------------------------------------------------------------------- /src/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/src/img/logo.png -------------------------------------------------------------------------------- /src/img/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengshi123/vue-ssr/5bad4028d676fab61f00b46f34d68e0028b9f0af/src/img/logout.png -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | // import UserManage from '../views/userManage.vue' 4 | // import MovieManage from '../views/movieManage.vue' 5 | // import DataAnalysis from '../views/dataAnalysis.vue' 6 | 7 | const UserManage = () => import('../views/userManage.vue') 8 | const MovieManage = () => import('../views/movieManage.vue') 9 | const DataAnalysis = () => import('../views/dataAnalysis.vue') 10 | 11 | Vue.use(Router) 12 | 13 | export function createRouter() { 14 | return new Router({ 15 | mode: 'history', 16 | routes: [ 17 | { 18 | path: '/userManage', 19 | name: 'userManage', 20 | component: UserManage 21 | }, 22 | { 23 | path: '/movieManage', 24 | name: 'movieManage', 25 | component: MovieManage 26 | }, 27 | { 28 | path: '/dataAnalysis', 29 | name: 'dataAnalysis', 30 | component: DataAnalysis 31 | }, 32 | { 33 | path: '/', 34 | name: 'userManage', 35 | component: UserManage 36 | }, 37 | ] 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | // store.js 2 | import Vue from 'vue' 3 | import Vuex from 'vuex' 4 | import axios from 'axios' 5 | 6 | Vue.use(Vuex); 7 | 8 | export function createStore () { 9 | return new Vuex.Store({ 10 | state: { 11 | userList: [], 12 | movieList: [], 13 | analysisList:[], 14 | }, 15 | getters:{ 16 | userList:state => state.userList, 17 | movieList:state => state.movieList, 18 | analysisList:state => state.analysisList, 19 | }, 20 | actions: { 21 | getUserlist ({ commit }) { 22 | return axios.get('/api/getUserlist').then((res) => { 23 | commit('setUserlist', res.data.userlist) 24 | }) 25 | }, 26 | getMovielist ({ commit }) { 27 | return axios.get('/api/getMovielist').then((res) => { 28 | commit('setMovielist', res.data.movielist) 29 | }) 30 | }, 31 | getAnalysis ({ commit }) { 32 | return axios.get('/api/getAnalysis').then((res) => { 33 | console.log('后端返回的数据...'); 34 | console.log(res); 35 | commit('setAnalysis', res.data); 36 | }) 37 | }, 38 | }, 39 | mutations: { 40 | setUserlist (state, list) { 41 | state.userList = list 42 | }, 43 | setMovielist (state, list) { 44 | state.movieList = list 45 | }, 46 | setAnalysis (state, list) { 47 | state.analysisList = list 48 | }, 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/util/filters.js: -------------------------------------------------------------------------------- 1 | export function host (url) { 2 | const host = url.replace(/^https?:\/\//, '').replace(/\/.*$/, '') 3 | const parts = host.split('.').slice(-3) 4 | if (parts[0] === 'www') parts.shift() 5 | return parts.join('.') 6 | } 7 | 8 | export function timeAgo (time) { 9 | const between = Date.now() / 1000 - Number(time) 10 | if (between < 3600) { 11 | return pluralize(~~(between / 60), ' minute') 12 | } else if (between < 86400) { 13 | return pluralize(~~(between / 3600), ' hour') 14 | } else { 15 | return pluralize(~~(between / 86400), ' day') 16 | } 17 | } 18 | 19 | function pluralize (time, label) { 20 | if (time === 1) { 21 | return time + label 22 | } 23 | return time + label + 's' 24 | } 25 | -------------------------------------------------------------------------------- /src/util/title.js: -------------------------------------------------------------------------------- 1 | function getTitle (vm) { 2 | const { title } = vm.$options 3 | if (title) { 4 | return typeof title === 'function' 5 | ? title.call(vm) 6 | : title 7 | } 8 | } 9 | 10 | const serverTitleMixin = { 11 | created () { 12 | const title = getTitle(this) 13 | if (title) { 14 | this.$ssrContext.title = `Vue HN 2.0 | ${title}` 15 | } 16 | } 17 | } 18 | 19 | const clientTitleMixin = { 20 | mounted () { 21 | const title = getTitle(this) 22 | if (title) { 23 | document.title = `Vue HN 2.0 | ${title}` 24 | } 25 | } 26 | } 27 | 28 | export default process.env.VUE_ENV === 'server' 29 | ? serverTitleMixin 30 | : clientTitleMixin 31 | -------------------------------------------------------------------------------- /src/views/adminContent.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/dataAnalysis.vue: -------------------------------------------------------------------------------- 1 | 9 | 12 | 27 | -------------------------------------------------------------------------------- /src/views/header.vue: -------------------------------------------------------------------------------- 1 | 20 | 23 | 28 | -------------------------------------------------------------------------------- /src/views/leftNavigation.vue: -------------------------------------------------------------------------------- 1 | 10 | 13 | 64 | -------------------------------------------------------------------------------- /src/views/menuContainer.vue: -------------------------------------------------------------------------------- 1 | 29 | 33 | 80 | -------------------------------------------------------------------------------- /src/views/movieManage.vue: -------------------------------------------------------------------------------- 1 | 27 | 31 | 62 | -------------------------------------------------------------------------------- /src/views/userManage.vue: -------------------------------------------------------------------------------- 1 | 35 | 39 | 40 | 73 | --------------------------------------------------------------------------------