Browse Source

前端独立

tbphp 2 years ago
commit
cc25d38507

+ 13 - 0
.babelrc

@@ -0,0 +1,13 @@
+{
+  "presets": [
+    ["es2015", { "modules": false }],
+    "stage-2"
+  ],
+  "plugins": ["transform-runtime"],
+  "comments": false,
+  "env": {
+    "test": {
+      "plugins": [ "istanbul" ]
+    }
+  }
+}

+ 9 - 0
.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+node_modules/
+npm-debug.log

+ 21 - 0
README.md

@@ -0,0 +1,21 @@
+# 1to-admin
+
+> A Vue.js project
+
+## Build Setup
+
+``` bash
+# install dependencies
+npm install
+
+# serve with hot reload at localhost:8080
+npm run dev
+
+# build for production with minification
+npm run build
+
+# build for production and view the bundle analyzer report
+npm run build --report
+```
+
+For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

+ 40 - 0
build/build.js

@@ -0,0 +1,40 @@
+// https://github.com/shelljs/shelljs
+require('./check-versions')()
+
+process.env.NODE_ENV = 'production'
+
+var ora = require('ora')
+var path = require('path')
+var chalk = require('chalk')
+var shell = require('shelljs')
+var webpack = require('webpack')
+var config = require('../config')
+var webpackConfig = require('./webpack.prod.conf')
+
+var spinner = ora('building for production...')
+spinner.start()
+
+var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
+shell.rm('-rf', assetsPath)
+shell.mkdir('-p', assetsPath)
+shell.config.silent = true
+shell.cp('-R', 'static/*', assetsPath)
+shell.config.silent = false
+
+webpack(webpackConfig, function (err, stats) {
+  spinner.stop()
+  if (err) throw err
+  process.stdout.write(stats.toString({
+    colors: true,
+    modules: false,
+    children: false,
+    chunks: false,
+    chunkModules: false
+  }) + '\n\n')
+
+  console.log(chalk.cyan('  Build complete.\n'))
+  console.log(chalk.yellow(
+    '  Tip: built files are meant to be served over an HTTP server.\n' +
+    '  Opening index.html over file:// won\'t work.\n'
+  ))
+})

+ 45 - 0
build/check-versions.js

@@ -0,0 +1,45 @@
+var chalk = require('chalk')
+var semver = require('semver')
+var packageConfig = require('../package.json')
+
+function exec (cmd) {
+  return require('child_process').execSync(cmd).toString().trim()
+}
+
+var versionRequirements = [
+  {
+    name: 'node',
+    currentVersion: semver.clean(process.version),
+    versionRequirement: packageConfig.engines.node
+  },
+  {
+    name: 'npm',
+    currentVersion: exec('npm --version'),
+    versionRequirement: packageConfig.engines.npm
+  }
+]
+
+module.exports = function () {
+  var warnings = []
+  for (var i = 0; i < versionRequirements.length; i++) {
+    var mod = versionRequirements[i]
+    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
+      warnings.push(mod.name + ': ' +
+        chalk.red(mod.currentVersion) + ' should be ' +
+        chalk.green(mod.versionRequirement)
+      )
+    }
+  }
+
+  if (warnings.length) {
+    console.log('')
+    console.log(chalk.yellow('To use this template, you must update following to modules:'))
+    console.log()
+    for (var i = 0; i < warnings.length; i++) {
+      var warning = warnings[i]
+      console.log('  ' + warning)
+    }
+    console.log()
+    process.exit(1)
+  }
+}

+ 9 - 0
build/dev-client.js

@@ -0,0 +1,9 @@
+/* eslint-disable */
+require('eventsource-polyfill')
+var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
+
+hotClient.subscribe(function (event) {
+  if (event.action === 'reload') {
+    window.location.reload()
+  }
+})

+ 81 - 0
build/dev-server.js

@@ -0,0 +1,81 @@
+require('./check-versions')()
+
+var config = require('../config')
+if (!process.env.NODE_ENV) {
+  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
+}
+
+var opn = require('opn')
+var path = require('path')
+var express = require('express')
+var webpack = require('webpack')
+var proxyMiddleware = require('http-proxy-middleware')
+var webpackConfig = require('./webpack.dev.conf')
+
+// default port where dev server listens for incoming traffic
+var port = process.env.PORT || config.dev.port
+// automatically open browser, if not set will be false
+var autoOpenBrowser = !!config.dev.autoOpenBrowser
+// Define HTTP proxies to your custom API backend
+// https://github.com/chimurai/http-proxy-middleware
+var proxyTable = config.dev.proxyTable
+
+var app = express()
+var compiler = webpack(webpackConfig)
+
+var devMiddleware = require('webpack-dev-middleware')(compiler, {
+  publicPath: webpackConfig.output.publicPath,
+  quiet: true
+})
+
+var hotMiddleware = require('webpack-hot-middleware')(compiler, {
+  log: () => {}
+})
+// force page reload when html-webpack-plugin template changes
+compiler.plugin('compilation', function (compilation) {
+  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
+    hotMiddleware.publish({ action: 'reload' })
+    cb()
+  })
+})
+
+// proxy api requests
+Object.keys(proxyTable).forEach(function (context) {
+  var options = proxyTable[context]
+  if (typeof options === 'string') {
+    options = { target: options }
+  }
+  app.use(proxyMiddleware(options.filter || context, options))
+})
+
+// handle fallback for HTML5 history API
+app.use(require('connect-history-api-fallback')())
+
+// serve webpack bundle output
+app.use(devMiddleware)
+
+// enable hot-reload and state-preserving
+// compilation error display
+app.use(hotMiddleware)
+
+// serve pure static assets
+var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
+app.use(staticPath, express.static('./static'))
+
+var uri = 'http://localhost:' + port
+
+devMiddleware.waitUntilValid(function () {
+  console.log('> Listening at ' + uri + '\n')
+})
+
+module.exports = app.listen(port, function (err) {
+  if (err) {
+    console.log(err)
+    return
+  }
+
+  // when env is testing, don't need open it
+  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
+    opn(uri)
+  }
+})

+ 71 - 0
build/utils.js

@@ -0,0 +1,71 @@
+var path = require('path')
+var config = require('../config')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+
+exports.assetsPath = function (_path) {
+  var assetsSubDirectory = process.env.NODE_ENV === 'production'
+    ? config.build.assetsSubDirectory
+    : config.dev.assetsSubDirectory
+  return path.posix.join(assetsSubDirectory, _path)
+}
+
+exports.cssLoaders = function (options) {
+  options = options || {}
+
+  var cssLoader = {
+    loader: 'css-loader',
+    options: {
+      minimize: process.env.NODE_ENV === 'production',
+      sourceMap: options.sourceMap
+    }
+  }
+
+  // generate loader string to be used with extract text plugin
+  function generateLoaders (loader, loaderOptions) {
+    var loaders = [cssLoader]
+    if (loader) {
+      loaders.push({
+        loader: loader + '-loader',
+        options: Object.assign({}, loaderOptions, {
+          sourceMap: options.sourceMap
+        })
+      })
+    }
+
+    // Extract CSS when that option is specified
+    // (which is the case during production build)
+    if (options.extract) {
+      return ExtractTextPlugin.extract({
+        use: loaders,
+        fallback: 'vue-style-loader'
+      })
+    } else {
+      return ['vue-style-loader'].concat(loaders)
+    }
+  }
+
+  // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
+  return {
+    css: generateLoaders(),
+    postcss: generateLoaders(),
+    less: generateLoaders('less'),
+    sass: generateLoaders('sass', { indentedSyntax: true }),
+    scss: generateLoaders('sass'),
+    stylus: generateLoaders('stylus'),
+    styl: generateLoaders('stylus')
+  }
+}
+
+// Generate loaders for standalone style files (outside of .vue)
+exports.styleLoaders = function (options) {
+  var output = []
+  var loaders = exports.cssLoaders(options)
+  for (var extension in loaders) {
+    var loader = loaders[extension]
+    output.push({
+      test: new RegExp('\\.' + extension + '$'),
+      use: loader
+    })
+  }
+  return output
+}

+ 17 - 0
build/vue-loader.conf.js

@@ -0,0 +1,17 @@
+var utils = require('./utils')
+var config = require('../config')
+var isProduction = process.env.NODE_ENV === 'production'
+
+module.exports = {
+  loaders: utils.cssLoaders({
+    sourceMap: isProduction
+      ? config.build.productionSourceMap
+      : config.dev.cssSourceMap,
+    extract: isProduction
+  }),
+  postcss: [
+    require('autoprefixer')({
+      browsers: ['last 2 versions']
+    })
+  ]
+}

+ 66 - 0
build/webpack.base.conf.js

@@ -0,0 +1,66 @@
+var path = require('path')
+var utils = require('./utils')
+var config = require('../config')
+var vueLoaderConfig = require('./vue-loader.conf')
+
+function resolve (dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+module.exports = {
+  entry: {
+    app: './src/main.js'
+  },
+  output: {
+    path: config.build.assetsRoot,
+    filename: '[name].js',
+    publicPath: process.env.NODE_ENV === 'production'
+      ? config.build.assetsPublicPath
+      : config.dev.assetsPublicPath
+  },
+  resolve: {
+    extensions: ['.js', '.vue', '.json'],
+    modules: [
+      resolve('src'),
+      resolve('node_modules')
+    ],
+    alias: {
+      'vue$': 'vue/dist/vue.common.js',
+      'src': resolve('src'),
+      'assets': resolve('src/assets'),
+      'components': resolve('src/components'),
+      'inc': resolve('src/components/inc'),
+      'tools': resolve('src/components/tools')
+    }
+  },
+  module: {
+    rules: [
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader',
+        include: [resolve('src'), resolve('test')]
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        query: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        query: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+        }
+      }
+    ]
+  }
+}

+ 35 - 0
build/webpack.dev.conf.js

@@ -0,0 +1,35 @@
+var utils = require('./utils')
+var webpack = require('webpack')
+var config = require('../config')
+var merge = require('webpack-merge')
+var baseWebpackConfig = require('./webpack.base.conf')
+var HtmlWebpackPlugin = require('html-webpack-plugin')
+var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
+
+// add hot-reload related code to entry chunks
+Object.keys(baseWebpackConfig.entry).forEach(function (name) {
+  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
+})
+
+module.exports = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
+  },
+  // cheap-module-eval-source-map is faster for development
+  devtool: '#cheap-module-eval-source-map',
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': config.dev.env
+    }),
+    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
+    new webpack.HotModuleReplacementPlugin(),
+    new webpack.NoEmitOnErrorsPlugin(),
+    // https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: 'index.html',
+      template: 'index.tpl',
+      inject: true
+    }),
+    new FriendlyErrorsPlugin()
+  ]
+})

+ 107 - 0
build/webpack.prod.conf.js

@@ -0,0 +1,107 @@
+var path = require('path')
+var utils = require('./utils')
+var webpack = require('webpack')
+var config = require('../config')
+var merge = require('webpack-merge')
+var baseWebpackConfig = require('./webpack.base.conf')
+var HtmlWebpackPlugin = require('html-webpack-plugin')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
+
+var env = config.build.env
+
+var webpackConfig = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.build.productionSourceMap,
+      extract: true
+    })
+  },
+  devtool: config.build.productionSourceMap ? '#source-map' : false,
+  output: {
+    path: config.build.assetsRoot,
+    filename: utils.assetsPath('js/[name].[chunkhash].js'),
+    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
+  },
+  plugins: [
+    // http://vuejs.github.io/vue-loader/en/workflow/production.html
+    new webpack.DefinePlugin({
+      'process.env': env
+    }),
+    new webpack.optimize.UglifyJsPlugin({
+      compress: {
+        warnings: false
+      },
+      sourceMap: true
+    }),
+    // extract css into its own file
+    new ExtractTextPlugin({
+      filename: utils.assetsPath('css/[name].[contenthash].css')
+    }),
+    // Compress extracted CSS. We are using this plugin so that possible
+    // duplicated CSS from different components can be deduped.
+    new OptimizeCSSPlugin(),
+    // generate dist index.html with correct asset hash for caching.
+    // you can customize output by editing /index.html
+    // see https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: config.build.index,
+      template: 'index.tpl',
+      inject: true,
+      minify: {
+        removeComments: true,
+        collapseWhitespace: true,
+        removeAttributeQuotes: true
+        // more options:
+        // https://github.com/kangax/html-minifier#options-quick-reference
+      },
+      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
+      chunksSortMode: 'dependency'
+    }),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+      minChunks: function (module, count) {
+        // any required modules inside node_modules are extracted to vendor
+        return (
+          module.resource &&
+          /\.js$/.test(module.resource) &&
+          module.resource.indexOf(
+            path.join(__dirname, '../node_modules')
+          ) === 0
+        )
+      }
+    }),
+    // extract webpack runtime and module manifest to its own file in order to
+    // prevent vendor hash from being updated whenever app bundle is updated
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'manifest',
+      chunks: ['vendor']
+    })
+  ]
+})
+
+if (config.build.productionGzip) {
+  var CompressionWebpackPlugin = require('compression-webpack-plugin')
+
+  webpackConfig.plugins.push(
+    new CompressionWebpackPlugin({
+      asset: '[path].gz[query]',
+      algorithm: 'gzip',
+      test: new RegExp(
+        '\\.(' +
+        config.build.productionGzipExtensions.join('|') +
+        ')$'
+      ),
+      threshold: 10240,
+      minRatio: 0.8
+    })
+  )
+}
+
+if (config.build.bundleAnalyzerReport) {
+  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
+}
+
+module.exports = webpackConfig

+ 7 - 0
config/dev.env.js

@@ -0,0 +1,7 @@
+var merge = require('webpack-merge')
+var prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+  NODE_ENV: '"development"',
+  API_URL: '"http://api.com/admin/"'
+})

+ 38 - 0
config/index.js

@@ -0,0 +1,38 @@
+// see http://vuejs-templates.github.io/webpack for documentation.
+var path = require('path')
+
+module.exports = {
+  build: {
+    env: require('./prod.env'),
+    index: path.resolve(__dirname, '../index.html'),
+    assetsRoot: path.resolve(__dirname, '../'),
+    assetsSubDirectory: 'static',
+    assetsPublicPath: '/',
+    productionSourceMap: false,
+    // Gzip off by default as many popular static hosts such as
+    // Surge or Netlify already gzip all static assets for you.
+    // Before setting to `true`, make sure to:
+    // npm install --save-dev compression-webpack-plugin
+    productionGzip: false,
+    productionGzipExtensions: ['js', 'css'],
+    // Run the build command with an extra argument to
+    // View the bundle analyzer report after build finishes:
+    // `npm run build --report`
+    // Set to `true` or `false` to always turn it on or off
+    bundleAnalyzerReport: process.env.npm_config_report
+  },
+  dev: {
+    env: require('./dev.env'),
+    port: 8080,
+    autoOpenBrowser: true,
+    assetsSubDirectory: 'static',
+    assetsPublicPath: '/',
+    proxyTable: {},
+    // CSS Sourcemaps off by default because relative paths are "buggy"
+    // with this option, according to the CSS-Loader README
+    // (https://github.com/webpack/css-loader#sourcemaps)
+    // In our experience, they generally work as expected,
+    // just be aware of this issue when enabling this option.
+    cssSourceMap: false
+  }
+}

+ 4 - 0
config/prod.env.js

@@ -0,0 +1,4 @@
+module.exports = {
+  NODE_ENV: '"production"',
+  API_URL: '"http://api.tbphp.net/admin/"'
+}

+ 10 - 0
index.tpl

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>admin</title>
+  </head>
+  <body>
+    <div id="app"></div>
+  </body>
+</html>

+ 58 - 0
package.json

@@ -0,0 +1,58 @@
+{
+  "name": "1to-admin",
+  "version": "1.0.0",
+  "description": "A Vue.js project",
+  "author": "",
+  "private": true,
+  "scripts": {
+    "dev": "node build/dev-server.js",
+    "build": "node build/build.js"
+  },
+  "dependencies": {
+    "element-ui": "^1.2.5",
+    "vue": "^2.2.5",
+    "vue-router": "^2.3.0",
+    "vuex": "^2.2.1"
+  },
+  "devDependencies": {
+    "autoprefixer": "^6.7.7",
+    "axios": "^0.15.3",
+    "babel-core": "^6.24.0",
+    "babel-loader": "^6.4.1",
+    "babel-plugin-transform-runtime": "^6.22.0",
+    "babel-preset-es2015": "^6.24.0",
+    "babel-preset-stage-2": "^6.22.0",
+    "babel-register": "^6.24.0",
+    "bootstrap": "^3.3.7",
+    "chalk": "^1.1.3",
+    "connect-history-api-fallback": "^1.3.0",
+    "css-loader": "^0.27.3",
+    "eventsource-polyfill": "^0.9.6",
+    "express": "^4.15.2",
+    "extract-text-webpack-plugin": "^2.1.0",
+    "file-loader": "^0.10.1",
+    "friendly-errors-webpack-plugin": "^1.6.1",
+    "function-bind": "^1.1.0",
+    "html-webpack-plugin": "^2.28.0",
+    "http-proxy-middleware": "^0.17.4",
+    "opn": "^4.0.2",
+    "optimize-css-assets-webpack-plugin": "^1.3.0",
+    "ora": "^1.2.0",
+    "promise-polyfill": "^6.0.2",
+    "semver": "^5.3.0",
+    "shelljs": "^0.7.7",
+    "url-loader": "^0.5.8",
+    "vue-loader": "^11.3.3",
+    "vue-style-loader": "^2.0.4",
+    "vue-template-compiler": "^2.2.5",
+    "webpack": "^2.3.2",
+    "webpack-bundle-analyzer": "^2.3.1",
+    "webpack-dev-middleware": "^1.10.0",
+    "webpack-hot-middleware": "^2.17.1",
+    "webpack-merge": "^4.1.0"
+  },
+  "engines": {
+    "node": ">= 4.0.0",
+    "npm": ">= 3.0.0"
+  }
+}

+ 15 - 0
src/App.vue

@@ -0,0 +1,15 @@
+<template>
+  <div id="app">
+    <template v-if="$store.state.base.islogin">
+      <router-view name="header" id="header"/>
+      <div id="body" class="el-row" :class="{off: !$store.state.base.switch}">
+        <router-view name="sidebar" class="sidebar el-col el-col-3"/>
+        <div class="main el-col el-col-21" v-loading="$store.state.base.loading">
+          <router-view name="main"/>
+        </div>
+      </div>
+      <router-view name="footer" id="footer"/>
+    </template>
+    <router-view v-else/>
+  </div>
+</template>

+ 32 - 0
src/assets/base.css

@@ -0,0 +1,32 @@
+/* 布局 */
+html,body,#app,#body,.sidebar,.main{height: 100%;margin: 0;position: static;overflow: hidden;}
+#header{height: 60px;background: #4a8bc2;}
+#body{padding-top: 60px;padding-bottom: 40px;margin-top: -60px;margin-bottom: -40px;}
+#footer{height: 40px;background: #f0f0f0;}
+#body .sidebar{border-radius: 0;transition: width .2s;}
+#body .main{padding: 20px;transition: width .2s;}
+#body.off .sidebar{width: 0;}
+#body.off .main{width: 100%;}
+.is-active .el-submenu__title{color: #20a0ff !important;}
+
+/* tabs */
+.tabs{height: 100%;overflow: hidden;}
+.el-tabs--card{height: 100%;}
+.el-tabs__content{height: 100%;margin-top: -57px;padding-top: 57px;box-sizing: border-box;overflow: hidden;position: static;}
+.el-tab-pane{overflow: auto;height: 100%;}
+
+/* 基础组件 */
+.list-table{margin-bottom: 10px;font-size: 13px;}
+.list-table td, .list-table th{height: 30px;}
+.list-table .cell, .list-table th>div{padding: 0 4px;}
+.list-table .el-tag{height: 20px;line-height: 18px;}
+.list-table .el-checkbox__inner{width: 14px;height: 14px;}
+.list-table .el-checkbox__inner::after{height: 6px;left: 4px;top: 1px;width: 2px;}
+.list-tag{margin-top: 4px;}
+.list-page{float: right;}
+.list-btn{border: 1px solid #bfcbd9 !important;font-size: 12px !important;}
+.btns{margin-bottom: 10px;}
+.searchForm .el-form-item{margin-bottom: 10px;}
+.login .el-input-group__prepend{width: 50px;text-align: center;}
+input.el-input__inner[readonly] {border: 0;background: #eee;}
+.el-select input.el-input__inner[readonly]{border: 1px solid #bfcbd9;background: #fff;}

BIN
src/assets/logo.png


+ 86 - 0
src/components/content/article.vue

@@ -0,0 +1,86 @@
+<template>
+  <div>
+    <t-list ref="list" api="article" select="status" :columns="columns" @dblclick="edit">
+      <template slot="searchForm" scope="{data}">
+        <el-form-item>
+          <el-input placeholder="标题" size="small" v-model="data.name"/>
+        </el-form-item>
+        <el-form-item>
+          <el-select placeholder="分类" size="small" v-model="data.status">
+            <el-option label="全部" value=""></el-option>
+            <el-option label="正常" value="y"></el-option>
+            <el-option label="已禁用" value="n"></el-option>
+          </el-select>
+        </el-form-item>
+      </template>
+      <template slot="bar" scope="{sel, sels}">
+        <el-button-group>
+          <t-btn type="add" @click="add"/>
+          <t-btn type="del" :sels="sels" @click="del"/>
+        </el-button-group>
+      </template>
+    </t-list>
+  </div>
+</template>
+
+<script>
+  export default {
+    data () {
+      return {
+        columns: [
+          {prop: 'title', label: '标题'},
+          {prop: 'type', label: '分类'},
+          {prop: 'nature', label: '性质'},
+          {prop: 'class', label: '归类'},
+          {prop: 'create_time', label: '发表时间'},
+          {prop: 'update_time', label: '更新时间'},
+          {label: '审核状态', dostr ({row}) {
+            if (row.audited == 'y') {
+              return '<span class="el-tag el-tag--success">已审核</span>'
+            }
+            return '<span class="el-tag el-tag--danger">未审核</span>'
+          }}
+        ],
+        api: 'article/'
+      }
+    },
+    methods: {
+      add () {
+        // this.userEdit.show()
+      },
+      del (ids) {
+        this.$confirm('此操作将删除选中的 '+ids.length+' 个用户, 是否继续?', '提示', {
+          type: 'warning',
+          confirmButtonText: '确定',
+          cancelButtonText: '取消'
+        }).then(() => {
+          this.$http.delete(this.api + ids.join('_')).then(() => {
+            this.list.reload()
+          }, err => {})
+        }, err => {})
+      },
+      edit (r) {
+        this.$http.get(this.api + r.id).then(data => {
+          // this.userEdit.show(data)
+        }, err => {})
+      },
+      enable (ids) {
+        this.$http.put(this.api + 'enable/' + ids.join('_')).then(() => {
+          this.list.reload()
+        })
+      },
+      disabled (ids) {
+        this.$http.put(this.api + 'disabled/' + ids.join('_')).then(() => {
+          this.list.reload()
+        })
+      },
+    },
+    mounted () {
+      // this.userEdit = this.$refs.useredit
+      this.list     = this.$refs.list
+    },
+    components: {
+      // articleEdit: require('./articleEdit')
+    }
+  }
+</script>

+ 11 - 0
src/components/inc/footer.vue

@@ -0,0 +1,11 @@
+<template>
+  <footer>
+    © 2017 1to.
+    &nbsp;&nbsp;&nbsp;&nbsp;
+    技术支持&nbsp;&nbsp;@tbphp
+  </footer>
+</template>
+
+<style scoped>
+  footer{line-height: 40px;text-align: center;color: #757575;font-size: 12px;}
+</style>

+ 47 - 0
src/components/inc/header.vue

@@ -0,0 +1,47 @@
+<template>
+  <header>
+    <div class="user">
+      <el-dropdown @command="doCmd">
+        <span>
+          {{$store.state.base.user.name}} <small>({{$store.state.base.user.username}})</small><i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="logout">退出登陆</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+    <div class="switch" :class="{on: $store.state.base.switch}" @click="$store.commit('base/switch')"><i class="el-icon-menu"></i></div>
+    <img class="logo" src="../../assets/logo.png">
+  </header>
+</template>
+
+<script>
+export default {
+  methods: {
+    logout () {
+      localStorage.removeItem('JWT_TOKEN')
+      window.location.reload()
+    },
+    doCmd (cmd) {
+      if (cmd == 'logout') {
+        this.$http.post('login/out').then(res => {
+          this.logout()
+        }).catch(err => {
+          this.logout()
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+  header{overflow: hidden;}
+  header .switch{height: 30px;width: 24px;transition: all .2s;margin-top: 30px;display: inline;float: left;line-height: 30px;text-align: center;color: #fff;font-size: 20px;cursor: pointer;}
+  header .on{background: #427dae;width: 80px;height: 60px;line-height: 60px;margin-top: 0;}
+  header img.logo{height: 50px;display: inline;float: left;margin-left: 20px;margin-top: 5px;}
+  header .user{display: inline;float: right;height: 30px;line-height: 30px;margin-top: 15px;color: #fff;margin-right: 20px;}
+  header .user span{color: #fff;font-size: 16px;cursor: pointer;}
+  header .user span i{font-size: 12px;margin-left: 8px;}
+  header .user small{color: #ccc;}
+</style>

+ 3 - 0
src/components/inc/main.vue

@@ -0,0 +1,3 @@
+<template>
+  <router-view></router-view>
+</template>

+ 9 - 0
src/components/inc/sidebar.vue

@@ -0,0 +1,9 @@
+<template>
+  <el-menu theme="dark" unique-opened router :default-active="$route.path">
+    <el-menu-item index="/"><i class="el-icon-star-on"></i>仪表盘</el-menu-item>
+    <el-submenu v-for="m in $store.state.base.menu" :key="m.name" :index="m.name">
+      <template slot="title"><i class="el-icon-document"></i>{{m.title}}</template>
+      <el-menu-item v-for="c in m.children" :key="m.name+'-'+c.name" :index="'/'+m.name+'/'+c.name">{{c.title}}</el-menu-item>
+    </el-submenu>
+  </el-menu>
+</template>

+ 79 - 0
src/components/inc/tabs.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="tabs">
+    <el-tabs v-model="name" type="card" @tab-click="seltab" @tab-remove="deltab">
+      <el-tab-pane name="/" key="/" :closable="false">
+        <span slot="label"><i class="el-icon-star-on"></i> 仪表盘</span>
+        <index></index>
+      </el-tab-pane>
+      <el-tab-pane v-for="tab in tabs" :name="tab.name" :key="tab.name" closable>
+      <span slot="label">{{tab.title}} <el-tag v-show="tab.name == name" type="gray" close-transition>{{tab.top}}</el-tag></span>
+        <div class="cont">
+          <keep-alive>
+            <component :is="tab.component"></component>
+          </keep-alive>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+  export default {
+    data () {
+      return {
+        name: '/',
+        tabs: []
+      }
+    },
+    methods: {
+      seltab (tab) {
+        this.$router.push(tab.name)
+      },
+      deltab (n) {
+        if (n == this.name) {
+          this.$router.push('/')
+        }
+        this.tabs = this.tabs.filter(({name}) => n !== name)
+      }
+    },
+    watch: {
+      $route () {
+        let {path} = this.$route
+        this.name = path
+        if (path == '/') {
+          return
+        }
+
+        for (let {name} of this.tabs) {
+          if (path == name) {
+            return
+          }
+        }
+        this.tabs.push({
+          name: path,
+          title: this.$route.meta.name,
+          top: this.$route.matched[1].meta.name,
+          component: this.$route.matched[2].components.default
+        })
+      }
+    },
+    created () {
+      if (this.$route.path != '/') {
+        this.tabs.push({
+          name: this.$route.path,
+          title: this.$route.meta.name,
+          top: this.$route.matched[1].meta.name,
+          component: this.$route.matched[2].components.default
+        })
+        this.name = this.$route.path
+      }
+    },
+    components: {
+      index: require('components/index')
+    }
+  }
+</script>
+
+<style scoped>
+  .el-tabs__item.is-active small{color: #8391a5}
+</style>

+ 3 - 0
src/components/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <h1>首页</h1>
+</template>

+ 68 - 0
src/components/login.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="login-main">
+    <div class="login">
+      <h1>后台系统登陆</h1>
+      <el-form @submit.native.prevent="login" :model="form" :rules="rules" ref="form">
+        <el-form-item prop="username">
+          <el-input auto-complete="off" v-model="form.username" autofocus>
+            <template slot="prepend">用户名</template>
+          </el-input>
+        </el-form-item>
+        <el-form-item prop="password">
+          <el-input type="password" auto-complete="off" v-model="form.password">
+            <template slot="prepend">密&nbsp;&nbsp;&nbsp;码</template>
+          </el-input>
+        </el-form-item>
+        <el-form-item class="login-text">
+          <el-switch v-model="form.keep"></el-switch> 保持登陆
+        </el-form-item>
+        <el-form-item>
+          <el-button type="success" class="btn" native-type="submit">登&nbsp;&nbsp;&nbsp;&nbsp;陆</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+  import Vue from 'vue'
+
+  export default {
+    data () {
+      return {
+        form: {
+          username: '',
+          password: '',
+          keep: false
+        },
+        rules: {
+          username: {required: true, message: '请输入用户名', trigger: 'submit'},
+          password: {required: true, message: '请输入密码', trigger: 'submit'}
+        }
+      }
+    },
+    methods: {
+      login () {
+        this.$refs.form.validate(r => {
+          if (r) {
+            this.$http.post('login/in', this.form).then(data => {
+              localStorage.JWT_TOKEN = data.token
+              this.$store.commit('base/setuser', {username: data.username, name: data.name})
+              this.$store.commit('base/setmenu', data.menu)
+              this.$store.commit('base/login')
+              this.$router.push('/')
+            }, err => {})
+          }
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+  .login-main{width: 100%;height: 100%;background: #eee;}
+  .login{position: absolute;left: 0;right: 0;top: 0;bottom: 0;display: block;width: 260px;height: 260px;margin: auto;padding: 35px;background: #fff;border-radius: 8px;box-shadow: 0 0 20px #a8a8a8;}
+  .login h1{margin: 0;font-weight: 400;color: #555;font-size: 22px;line-height: 30px;margin-bottom: 20px;text-align: center;}
+  .login .btn{width: 100%;}
+  .login-text{color: #99a9bf;}
+</style>

+ 103 - 0
src/components/system/user.vue

@@ -0,0 +1,103 @@
+<template>
+  <div>
+    <t-list ref="list" api="user" select="status" :columns="columns" @dblclick="edit">
+      <template slot="searchForm" scope="{data}">
+        <el-form-item>
+          <el-input placeholder="姓名" size="small" v-model="data.name"/>
+        </el-form-item>
+        <el-form-item>
+          <el-select placeholder="状态" size="small" v-model="data.status">
+            <el-option label="全部" value=""></el-option>
+            <el-option label="正常" value="y"></el-option>
+            <el-option label="已禁用" value="n"></el-option>
+          </el-select>
+        </el-form-item>
+      </template>
+      <template slot="bar" scope="{sel, sels}">
+        <el-button-group>
+          <t-btn type="add" @click="add"/>
+          <t-btn type="del" :sels="sels" @click="del"/>
+        </el-button-group>
+        <el-button-group>
+          <t-btn type="enable" :sels="sels" @click="enable"></t-btn>
+          <t-btn type="disabled" :sels="sels" @click="disabled"></t-btn>
+        </el-button-group>
+        <el-button type="info" icon="information" size="small" :disabled="!sel" @click="changePwd(sel)">重置密码</el-button>
+      </template>
+    </t-list>
+    <user-edit ref="useredit" @reload="$refs.list.reload()"></user-edit>
+  </div>
+</template>
+
+<script>
+  export default {
+    data () {
+      return {
+        columns: [
+          {prop: 'username', label: '账号'},
+          {prop: 'name', label: '姓名'},
+          {prop: 'tel', label: '电话'},
+          {prop: 'age', label: '年龄'},
+          {prop: 'sex', label: '性别'},
+          {prop: 'login_time', label: '登陆时间'},
+          {prop: 'login_ip', label: '登陆ip'},
+          {label: '状态', dostr ({row}) {
+            if (row.status == 'y') {
+              return '<span class="el-tag el-tag--success">正常</span>'
+            }
+            return '<span class="el-tag el-tag--danger">已禁用</span>'
+          }}
+        ]
+      }
+    },
+    methods: {
+      add () {
+        this.userEdit.show()
+      },
+      del (ids) {
+        this.$confirm('此操作将删除选中的 '+ids.length+' 个用户, 是否继续?', '提示', {
+          type: 'warning',
+          confirmButtonText: '确定',
+          cancelButtonText: '取消'
+        }).then(() => {
+          this.$http.delete('user/' + ids.join('_')).then(() => {
+            this.list.reload()
+          }, err => {})
+        }, err => {})
+      },
+      edit (r) {
+        this.$http.get('user/' + r.id).then(data => {
+          this.userEdit.show(data)
+        }, err => {})
+      },
+      enable (ids) {
+        this.$http.put('user/enable/' + ids.join('_')).then(() => {
+          this.list.reload()
+        })
+      },
+      disabled (ids) {
+        this.$http.put('user/disabled/' + ids.join('_')).then(() => {
+          this.list.reload()
+        })
+      },
+      changePwd (r) {
+        console.info(r)
+        this.$prompt('请输入新密码', '修改密码', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消'
+        }).then(({value}) => {
+          this.$http.put('user/' + r.id, {password: value}).then(() => {
+            this.list.reload()
+          }, err => {})
+        }, err => {})
+      }
+    },
+    mounted () {
+      this.userEdit = this.$refs.useredit
+      this.list     = this.$refs.list
+    },
+    components: {
+      userEdit: require('./userEdit')
+    }
+  }
+</script>

+ 95 - 0
src/components/system/userEdit.vue

@@ -0,0 +1,95 @@
+<template>
+  <el-dialog title="提示" v-model="open" size="tiny" @close="$refs.form.resetFields()">
+    <el-form :model="form" ref="form" label-width="100px" :rules="rules">
+      <el-form-item label="用户名" prop="username">
+        <el-input v-model="form.username" :readonly="isedit"/>
+      </el-form-item>
+      <el-form-item label="姓名" prop="name">
+        <el-input v-model="form.name"/>
+      </el-form-item>
+      <el-form-item label="年龄">
+        <el-input-number v-model.number="form.age" :min="0" :max="100"/>
+      </el-form-item>
+      <el-form-item label="性别">
+        <el-radio v-model="form.sex" label="男">男</el-radio>
+        <el-radio v-model="form.sex" label="女">女</el-radio>
+      </el-form-item>
+      <el-form-item label="电话">
+        <el-input v-model.number="form.tel"/>
+      </el-form-item>
+      <template v-if="isedit">
+        <el-form-item label="登陆时间">
+          {{form.login_time}}
+        </el-form-item>
+        <el-form-item label="登陆IP">
+          {{form.login_ip}}
+        </el-form-item>
+        <el-form-item label="状态">
+          <span v-if="form.status == 'y'" class="el-tag el-tag--success">正常</span>
+          <span v-else class="el-tag el-tag--danger">已禁用</span>
+        </el-form-item>
+      </template>
+    </el-form>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="open = false">取 消</el-button>
+      <el-button v-if="isedit" type="primary" @click="update">更 新</el-button>
+      <el-button v-else type="primary" @click="save">提 交</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+  export default {
+    data () {
+      return {
+        open: false,
+        isedit: false,
+        form: {
+          username: '',
+          name: '',
+          age: '',
+          sex: '男',
+          tel: ''
+        },
+        rules: {
+          username: [
+            {required: true, message: '请输入用户名'},
+            {min: 4, max: 12, message: '请输入4-12个字符'}
+          ],
+          name: {required: true, message: '请输入姓名'}
+        }
+      }
+    },
+    methods: {
+      save () {
+        this.$refs.form.validate(r => {
+          if (r) {
+            this.$http.post('user', this.form).then(msg => {
+              this.open = false
+              this.$emit('reload')
+            })
+          }
+        })
+      },
+      update () {
+        this.$refs.form.validate(r => {
+          if (r) {
+            this.$http.put('user/' + this.form.id, this.form).then(data => {
+              this.open = false
+              this.$emit('reload')
+            }, err => {})
+          }
+        })
+      },
+      show (editData) {
+        this.open = true
+        if (editData) {
+          this.isedit = true
+        } else {
+          this.isedit = false
+        }
+        this.form = editData || this.$options.data().form
+      }
+    }
+  }
+</script>

+ 57 - 0
src/components/tools/tBtn.vue

@@ -0,0 +1,57 @@
+<template>
+  <el-button :type="btn.type" :icon="btn.icon" size="small" :disabled="disabled" @click="click">{{btn.title}}</el-button>
+</template>
+
+<script>
+  export default {
+    props: {
+      type: {
+        type: String,
+        required: true
+      },
+      sel: Object,
+      sels: Array,
+      field: {
+        type: String,
+        default: 'id'
+      }
+    },
+    data () {
+      let btns = {
+        add: {type: 'success', icon: 'plus', title: '新增'},
+        del: {type: 'danger', icon: 'delete', title: '删除'},
+        enable: {type: 'primary', icon: 'circle-check', title: '启用'},
+        disabled: {type: 'warning', icon: 'circle-close', title: '禁用'}
+      }
+      return {
+        btn: btns[this.type],
+        hasSel: this.sel !== undefined,
+        hasSels: this.sels !== undefined
+      }
+    },
+    computed: {
+      disabled () {
+        if (this.hasSel) {
+          return !this.sel
+        } else if (this.hasSels) {
+          return !this.sels || this.sels.length < 1
+        } else {
+          return false
+        }
+      }
+    },
+    methods: {
+      click () {
+        let data
+        if (this.hasSels) {
+          let ids = []
+          this.sels.forEach(v => ids.push(v[this.field]))
+          data = ids
+        } else if (this.hasSel) {
+          data = this.sel[this.field]
+        }
+        this.$emit('click', data)
+      }
+    }
+  }
+</script>

+ 162 - 0
src/components/tools/tList.vue

@@ -0,0 +1,162 @@
+<template>
+  <div>
+    <el-form v-if="!!$scopedSlots['searchForm']" :model="searchData" class="searchForm" inline @submit.native.prevent="searchForm">
+      <slot name="searchForm" :data="searchData"></slot>
+      <el-form-item>
+        <el-button type="primary" size="small" native-type="submit" icon="search">查询</el-button>
+      </el-form-item>
+    </el-form>
+    <div class="btns" v-if="!!$scopedSlots['bar']">
+      <slot name="bar" :sels="sels" :sel="sel"></slot>
+    </div>
+    <el-table
+      class="list-table"
+      ref="table"
+      :data="list"
+      :max-height="481"
+      row-key="id"
+      highlight-current-row
+      border
+      @row-click="selrow"
+      @row-dblclick="dblclick"
+      @current-change="changeSel"
+      @selection-change="changeSels">
+      <el-table-column align="center" :width="30" type="selection" reserve-selection></el-table-column>
+      <el-table-column align="center" :width="40" label="#">
+        <template scope="scope">
+          {{(page - 1) * size + scope.$index + 1}}
+        </template>
+      </el-table-column>
+      <template v-for="c in columns">
+        <template v-if="c.dostr === undefined">
+          <el-table-column
+            align="center"
+            :width="c.width"
+            :prop="c.prop"
+            :label="c.label"
+            :key="c.prop"
+            :formatter="c.formatter"
+          />
+        </template>
+        <template v-else>
+          <el-table-column
+            align="center"
+            :width="c.width"
+            :prop="c.prop"
+            :label="c.label"
+            :key="c.prop"
+            :formatter="c.formatter"
+          >
+            <template scope="r">
+              <div v-html="c.dostr(r)"></div>
+            </template>
+          </el-table-column>
+        </template>
+      </template>
+
+    </el-table>
+    <el-pagination
+      class="list-page"
+      :total="total"
+      layout="slot, total, sizes, prev, pager, next, jumper"
+      @current-change="currentChange"
+      @size-change="sizeChange"
+    >
+      <el-button class="list-btn" size="small" :loading="loading" @click="load">刷新</el-button>
+    </el-pagination>
+    <el-tag v-show="selsnum > 0" class="list-tag" type="primary" :closable="true" @close="$refs.table.clearSelection()">当前选中 <strong>{{selsnum}}</strong> 条</el-tag>
+  </div>
+</template>
+
+<script>
+  export default {
+    props: {
+      api: {
+        type: String,
+        required: true
+      },
+      columns: {
+        type: Array,
+        required: true
+      },
+      // select,用于解决select没有初始对象值的问题
+      select: {
+        type: String
+      }
+    },
+    data () {
+      // 获取默认搜索字段
+      let searchData = {}
+      if (this.select) {
+        this.select.split(',').forEach(v => {
+          searchData[v.trim()] = undefined
+        })
+      }
+      return {
+        total: 0,
+        list: [],
+        page: 1,
+        size: 10,
+        loading: false,
+        sel: null,
+        sels: null,
+        searchData
+      }
+    },
+    computed: {
+      selsnum () {
+        if (this.sels) {
+          return this.sels.length
+        }
+        return 0
+      }
+    },
+    methods: {
+      load () {
+        this.loading = true
+        let params = {page: this.page, size: this.size}
+        Object.assign(params, this.searchData)
+        this.$http.get(this.api, {params: params}).then(data => {
+          data.loading = false
+          Object.assign(this.$data, data)
+        }, err => {
+          this.loading = false
+        })
+      },
+      searchForm () {
+        this.page = 1
+        this.load()
+      },
+      currentChange (page) {
+        this.page = page
+        this.load()
+      },
+      sizeChange (size) {
+        this.size = size
+        let totalPage = Math.ceil(this.total / size)
+        if (this.page <= totalPage) {
+          this.load()
+        }
+      },
+      selrow (r) {
+        this.$refs.table.toggleRowSelection(r)
+      },
+      dblclick (r) {
+        this.$emit('dblclick', r)
+      },
+      changeSel (s) {
+        this.sel = s
+      },
+      changeSels (s) {
+        this.sels = s
+      },
+      reload () {
+        this.load()
+        this.$refs.table.clearSelection()
+      }
+    },
+    created () {
+      this.load()
+    }
+  }
+</script>

+ 17 - 0
src/lib/fuc.js

@@ -0,0 +1,17 @@
+import App from '../App'
+import router from './router'
+
+export default {
+  install (Vue) {
+    // 项目初始化
+    Vue.init = store => {
+      new Vue({
+        el: '#app',
+        store,
+        router,
+        template: '<App/>',
+        components: {App}
+      })
+    }
+  }
+}

+ 105 - 0
src/lib/http.js

@@ -0,0 +1,105 @@
+import http from 'axios'
+import {Message} from 'element-ui'
+import store from '../store'
+
+let errmsg = null
+
+function error (msg) {
+  if (errmsg) {
+    errmsg.close()
+  }
+  errmsg = Message({
+    type: 'error',
+    duration: 0,
+    showClose: true,
+    message: msg
+  })
+}
+
+function success (msg) {
+  if (errmsg) {
+    errmsg.close()
+  }
+  errmsg = Message({
+    type: 'success',
+    message: msg
+  })
+}
+
+// 设置根url
+http.defaults.baseURL = process.env.API_URL
+
+// 请求拦截
+http.interceptors.request.use(config => {
+  // 开启loding
+  store.commit('base/loading', true)
+
+  // 如果已经登录,带上token
+  if (localStorage.JWT_TOKEN) {
+    config.headers = {'Authorization': localStorage.JWT_TOKEN}
+  }
+  return config
+}, err => {
+  store.commit('base/loading', false)
+  console.error(err)
+})
+
+// 返回拦截
+http.interceptors.response.use(res => {
+  store.commit('base/loading', false)
+
+  // token刷新
+  if (res.data.new_token) {
+    localStorage.JWT_TOKEN = res.data.new_token
+  }
+
+  // 返回状态处理
+  if (res.data.code == 0) {
+    error(res.data.msg)
+    throw new Error(res.data.msg)
+  } else if (res.data.code) {
+    switch (res.data.code) {
+      case 1: // 成功-返回数据
+        if (typeof res.data.data == 'string') {
+          success(res.data.data)
+        } else if (res.data.msg) {
+          success(res.data.msg)
+        }
+        return res.data.data
+
+      case 4: // 验证失败
+        error(res.data.msg)
+
+      case 401: // 已退出-清楚token
+        Message({
+          type: 'error',
+          message: '系统已退出,请重新登陆'
+        })
+        localStorage.removeItem('JWT_TOKEN')
+        this.$store.commit('base/logout')
+        this.$router.push('/login')
+        break
+
+      default:
+        if (res.data.msg) {
+          error(res.data.msg)
+        }
+    }
+  } else {
+    error('服务器错误')
+    throw new Error('服务器错误')
+  }
+}, err => {
+  store.commit('base/loading', false)
+  error('网络错误,请稍后再试!')
+  throw new Error(err.message)
+})
+
+export default {
+  install (Vue) {
+    Vue.prototype.$http = http
+    Vue.http = http
+  }
+}
+
+export {http}

+ 88 - 0
src/lib/router.js

@@ -0,0 +1,88 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import store from '../store'
+
+Vue.use(Router)
+
+function include (name) {
+  return resolve => require(['components/' + name], resolve)
+}
+
+let tabs = include('inc/tabs')
+let options = {
+  mode: 'history',
+  routes: [
+    {
+      path: '/',
+      meta: {name: '首页'},
+      components: {
+        header: include('inc/header'),
+        sidebar: include('inc/sidebar'),
+        main: tabs,
+        footer: include('inc/footer')
+      },
+      children: []
+    },
+    {
+      path: '/login',
+      component: include('login')
+    },
+    {
+      path: '*',
+      redirect: '/'
+    }
+  ]
+}
+
+// 注册路由方法
+let main = include('inc/main')
+function rule (path, name) {
+  let info = path.split('/')
+  if (info.length == 1) {
+    options.routes[0].children.push({
+      path: path,
+      component: main,
+      meta: {name: name}
+    })
+  } else if (info.length == 2) {
+    for (let i in options.routes[0].children) {
+      if (options.routes[0].children[i].path == info[0]) {
+        if (!options.routes[0].children[i].children) {
+          options.routes[0].children[i].children = []
+        }
+
+        let base = {
+          path: info[1],
+          component: include(path),
+          meta: {name: name}
+        }
+        options.routes[0].children[i].children.push(base)
+        break
+      }
+    }
+  }
+}
+
+rule('system', '系统设置')
+rule('system/user', '用户管理')
+rule('content', '内容管理')
+rule('content/article', '文章')
+let router = new Router(options)
+
+router.beforeEach(({path}, from, next) => {
+  if (store.state.base.islogin) {
+    if (path == '/login') {
+      next('/')
+    } else {
+      next()
+    }
+  } else {
+    if (path == '/login') {
+      next()
+    } else {
+      next('/login')
+    }
+  }
+})
+
+export default router

+ 6 - 0
src/lib/tools.js

@@ -0,0 +1,6 @@
+export default {
+  install (Vue) {
+    Vue.component('tList', require('tools/tList'))
+    Vue.component('tBtn', require('tools/tBtn'))
+  }
+}

+ 39 - 0
src/main.js

@@ -0,0 +1,39 @@
+import 'element-ui/lib/theme-default/index.css'
+import 'assets/base.css'
+import Vue from 'vue'
+import ElementUI from 'element-ui'
+import http from 'lib/http'
+import tools from 'lib/tools'
+import fuc from 'lib/fuc'
+import store from './store'
+
+// 兼容IE Promise
+if (!window.Promise) {
+  window.Promise = require('promise-polyfill')
+}
+// 注册ele-ui组件
+Vue.use(ElementUI)
+
+// 注册异步请求插件
+Vue.use(http)
+
+// 导入工具组件
+Vue.use(tools)
+
+// 导入公共方法库
+Vue.use(fuc)
+
+// 验证token合法性
+if (localStorage.JWT_TOKEN) {
+  Vue.http.post('login/check', null, {headers: {'Authorization': localStorage.JWT_TOKEN}}).then(res => {
+    store.commit('base/setuser', {username: res.username, name: res.name})
+    store.commit('base/setmenu', res.menu)
+    store.commit('base/login')
+    Vue.init(store)
+  }, err => {
+    localStorage.removeItem('JWT_TOKEN')
+    Vue.init(store)
+  })
+} else {
+  Vue.init(store)
+}

+ 21 - 0
src/store/index.js

@@ -0,0 +1,21 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+// 基础模块
+import base from './modules/base'
+
+// 业务模块
+
+// 兼容IE Promise
+if (!window.Promise) {
+  window.Promise = require('promise-polyfill')
+}
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  strict: process.env.NODE_ENV !== 'production',
+  modules: {
+    base
+  }
+})

+ 31 - 0
src/store/modules/base.js

@@ -0,0 +1,31 @@
+const pre = 'base/'
+
+export default {
+  state: {
+    islogin: false,
+    user: '',
+    menu: [],
+    loading: false,
+    switch: true
+  },
+  mutations: {
+    [pre + 'login'] (state) {
+      state.islogin = true
+    },
+    [pre + 'logout'] (state) {
+      state.islogin = false
+    },
+    [pre + 'setuser'] (state, user) {
+      state.user = user
+    },
+    [pre + 'setmenu'] (state, data) {
+      state.menu = data
+    },
+    [pre + 'loading'] (state, load) {
+      state.loading = load
+    },
+    [pre + 'switch'] (state) {
+      state.switch = !state.switch
+    }
+  }
+}