├── .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 |
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 |
2 |
7 |
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 |
4 |
10 |
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 |
2 |
9 |
10 |
11 |
14 |
--------------------------------------------------------------------------------
/src/views/dataAnalysis.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
电影类别统计
4 |
5 |
![]()
6 |
7 |
8 |
9 |
12 |
27 |
--------------------------------------------------------------------------------
/src/views/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
16 | 欢迎您,系统管理员!
17 |
18 |
19 |
20 |
23 |
28 |
--------------------------------------------------------------------------------
/src/views/leftNavigation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
后台管理
6 |
7 |
8 |
9 |
10 |
13 |
64 |
--------------------------------------------------------------------------------
/src/views/menuContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
33 |
80 |
--------------------------------------------------------------------------------
/src/views/movieManage.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
电影数据管理
7 |
8 | 导入电影数据集
9 | 新建电影
10 | 删除电影
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 编辑
21 | 删除
22 |
23 |
24 |
25 |
26 |
27 |
31 |
62 |
--------------------------------------------------------------------------------
/src/views/userManage.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
用户数据管理
7 |
8 | 导入用户数据集
9 | 新建用户
10 | 删除用户
11 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
22 | {{scope.row.sex == 'F'? '女' : '男'}}
23 |
24 |
25 |
26 |
27 |
28 | 编辑
29 | 删除
30 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
73 |
--------------------------------------------------------------------------------