GuoYuFu123 пре 6 година
комит
52ced77fac
49 измењених фајлова са 3253 додато и 0 уклоњено
  1. 12 0
      .babelrc
  2. 9 0
      .editorconfig
  3. 14 0
      .gitignore
  4. 10 0
      .postcssrc.js
  5. 89 0
      README.md
  6. 41 0
      build/build.js
  7. 54 0
      build/check-versions.js
  8. BIN
      build/logo.png
  9. 101 0
      build/utils.js
  10. 22 0
      build/vue-loader.conf.js
  11. 82 0
      build/webpack.base.conf.js
  12. 95 0
      build/webpack.dev.conf.js
  13. 145 0
      build/webpack.prod.conf.js
  14. 9 0
      config/dev.env.js
  15. 75 0
      config/index.js
  16. 5 0
      config/prod.env.js
  17. 11 0
      index.html
  18. 72 0
      package.json
  19. 11 0
      src/App.vue
  20. 6 0
      src/api/system/admin.js
  21. BIN
      src/assets/logo-small.jpg
  22. BIN
      src/assets/logo.jpg
  23. 53 0
      src/components/BasePagination.vue
  24. 401 0
      src/data/menus.js
  25. 24 0
      src/framework/layout/Login.vue
  26. 64 0
      src/framework/layout/Main.vue
  27. 75 0
      src/framework/layout/PageHeader.vue
  28. 77 0
      src/framework/layout/PageSidebar.vue
  29. 140 0
      src/framework/layout/layout-store.js
  30. 18 0
      src/framework/style/awesome.less
  31. 346 0
      src/framework/style/layout.scss
  32. 65 0
      src/framework/style/theme.scss
  33. 32 0
      src/framework/template/TablePageTemplate.vue
  34. 27 0
      src/main.js
  35. 40 0
      src/plugins/components.js
  36. 17 0
      src/router/demo.js
  37. 19 0
      src/router/index.js
  38. 149 0
      src/router/sagacloud.js
  39. 39 0
      src/store/index.js
  40. 118 0
      src/utils/authutils.js
  41. 102 0
      src/utils/basicutils.js
  42. 106 0
      src/utils/httputils.js
  43. 33 0
      src/utils/storage.js
  44. 24 0
      src/views/dasboard/index.vue
  45. 155 0
      src/views/demo/FormPageDemo.vue
  46. 123 0
      src/views/demo/TablePageDemo.vue
  47. 40 0
      src/views/system/auth/index.vue
  48. 103 0
      src/views/system/pwd/ChangePwd.vue
  49. 0 0
      static/.gitkeep

+ 12 - 0
.babelrc

@@ -0,0 +1,12 @@
+{
+  "presets": [
+    ["env", {
+      "modules": false,
+      "targets": {
+        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
+      }
+    }],
+    "stage-2"
+  ],
+  "plugins": ["transform-vue-jsx", "transform-runtime"]
+}

+ 9 - 0
.editorconfig

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

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+.DS_Store
+node_modules/
+/dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 10 - 0
.postcssrc.js

@@ -0,0 +1,10 @@
+// https://github.com/michael-ciniawsky/postcss-load-config
+
+module.exports = {
+  "plugins": {
+    "postcss-import": {},
+    "postcss-url": {},
+    // to edit target browsers: use "browserslist" field in package.json
+    "autoprefixer": {}
+  }
+}

+ 89 - 0
README.md

@@ -0,0 +1,89 @@
+# sagacloud-admin
+
+> A Vue.js project
+
+## Build Setup
+
+```bash
+# install dependencies
+npm install
+npm install vuex axios element-ui echarts font-awesome
+
+# serve with hot reload at localhost:8080
+npm run dev
+npm install less less-loader node-sass sass-loader --save-dev
+
+# build for production with minification
+npm run build
+
+# build for production and view the bundle analyzer report
+npm run build --report
+```
+
+For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
+
+#### 目录结构
+
+```
+├─api                       服务端接口API
+│
+├─components                通用组件
+│
+├─data                      静态数据
+│      menus.js             页面菜单文件,在这里新增菜单
+│
+├─framework                 页面框架, 不用修改
+│
+├─plugins                   通用插件
+│
+├─router                    路由
+│      index.js
+│      sagacloud.js         业务路由文件,在这里新增路由
+│
+├─store                     VUEX
+│      index.js
+│
+├─utils                     通用工具类
+│      authutils.js         认证工具, 不要修改
+│      basicutils.js
+│      httputils.js
+│      storage.js
+│
+└─views                     业务视图主目录, 这里创建新视图
+    ├─dasboard
+    └─system
+```
+
+#### 工具类说明
+
+/src/utils/basicutils.js
+
+```
+    formatDateByPattern(date, pattern) // 日期格式化
+    indexInArray(arr, key, val)        // 找出数组arr中第一个属性key的值等于val的元素的下标
+    itemInArray(arr, key, val)         // 找出数组arr中第一个属性key的值等于val的元素
+    deleteInArray(arr, key, val)       // 从数组中将属性key的值等于val的所有元素删除
+    hasPermission(promiseStr)          //判断该用户的权限是否具有某些权限
+```
+
+/src/utils/httputils.js // 一般在服务端接口 API 中使用,不建议在别的地方使用,有授权标记
+
+```
+    getCookie(name)                    // 获取cookie值
+    getJson(url, params)               // 发送GET请求
+    postJson(url, data)                // 发送POST请求
+```
+
+/src/utils/storage.js
+
+```
+    set(key, value, fn)                // 在浏览器缓存中设置数据, 如果value是对象会转化成json
+    get(key, fn)                       // 在浏览器缓存中取出数据, 取出数据后会尝试js对象返回
+    remove(key)                        // 在浏览器缓存中删除数据
+```
+
+#### 设置面包屑方法
+
+```
+    this.$store.dispatch('setBreadcrumb', [{ label: 'Demo' }, { label: 'Form' }])
+```

+ 41 - 0
build/build.js

@@ -0,0 +1,41 @@
+'use strict'
+require('./check-versions')()
+
+process.env.NODE_ENV = 'production'
+
+const ora = require('ora')
+const rm = require('rimraf')
+const path = require('path')
+const chalk = require('chalk')
+const webpack = require('webpack')
+const config = require('../config')
+const webpackConfig = require('./webpack.prod.conf')
+
+const spinner = ora('building for production...')
+spinner.start()
+
+rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
+  if (err) throw err
+  webpack(webpackConfig, (err, stats) => {
+    spinner.stop()
+    if (err) throw err
+    process.stdout.write(stats.toString({
+      colors: true,
+      modules: false,
+      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
+      chunks: false,
+      chunkModules: false
+    }) + '\n\n')
+
+    if (stats.hasErrors()) {
+      console.log(chalk.red('  Build failed with errors.\n'))
+      process.exit(1)
+    }
+
+    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'
+    ))
+  })
+})

+ 54 - 0
build/check-versions.js

@@ -0,0 +1,54 @@
+'use strict'
+const chalk = require('chalk')
+const semver = require('semver')
+const packageConfig = require('../package.json')
+const shell = require('shelljs')
+
+function exec (cmd) {
+  return require('child_process').execSync(cmd).toString().trim()
+}
+
+const versionRequirements = [
+  {
+    name: 'node',
+    currentVersion: semver.clean(process.version),
+    versionRequirement: packageConfig.engines.node
+  }
+]
+
+if (shell.which('npm')) {
+  versionRequirements.push({
+    name: 'npm',
+    currentVersion: exec('npm --version'),
+    versionRequirement: packageConfig.engines.npm
+  })
+}
+
+module.exports = function () {
+  const warnings = []
+
+  for (let i = 0; i < versionRequirements.length; i++) {
+    const 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 (let i = 0; i < warnings.length; i++) {
+      const warning = warnings[i]
+      console.log('  ' + warning)
+    }
+
+    console.log()
+    process.exit(1)
+  }
+}


+ 101 - 0
build/utils.js

@@ -0,0 +1,101 @@
+'use strict'
+const path = require('path')
+const config = require('../config')
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
+const packageConfig = require('../package.json')
+
+exports.assetsPath = function (_path) {
+  const assetsSubDirectory = process.env.NODE_ENV === 'production'
+    ? config.build.assetsSubDirectory
+    : config.dev.assetsSubDirectory
+
+  return path.posix.join(assetsSubDirectory, _path)
+}
+
+exports.cssLoaders = function (options) {
+  options = options || {}
+
+  const cssLoader = {
+    loader: 'css-loader',
+    options: {
+      sourceMap: options.sourceMap
+    }
+  }
+
+  const postcssLoader = {
+    loader: 'postcss-loader',
+    options: {
+      sourceMap: options.sourceMap
+    }
+  }
+
+  // generate loader string to be used with extract text plugin
+  function generateLoaders (loader, loaderOptions) {
+    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [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)
+    }
+  }
+
+  // https://vue-loader.vuejs.org/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) {
+  const output = []
+  const loaders = exports.cssLoaders(options)
+
+  for (const extension in loaders) {
+    const loader = loaders[extension]
+    output.push({
+      test: new RegExp('\\.' + extension + '$'),
+      use: loader
+    })
+  }
+
+  return output
+}
+
+exports.createNotifierCallback = () => {
+  const notifier = require('node-notifier')
+
+  return (severity, errors) => {
+    if (severity !== 'error') return
+
+    const error = errors[0]
+    const filename = error.file && error.file.split('!').pop()
+
+    notifier.notify({
+      title: packageConfig.name,
+      message: severity + ': ' + error.name,
+      subtitle: filename || '',
+      icon: path.join(__dirname, 'logo.png')
+    })
+  }
+}

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

@@ -0,0 +1,22 @@
+'use strict'
+const utils = require('./utils')
+const config = require('../config')
+const isProduction = process.env.NODE_ENV === 'production'
+const sourceMapEnabled = isProduction
+  ? config.build.productionSourceMap
+  : config.dev.cssSourceMap
+
+module.exports = {
+  loaders: utils.cssLoaders({
+    sourceMap: sourceMapEnabled,
+    extract: isProduction
+  }),
+  cssSourceMap: sourceMapEnabled,
+  cacheBusting: config.dev.cacheBusting,
+  transformToRequire: {
+    video: ['src', 'poster'],
+    source: 'src',
+    img: 'src',
+    image: 'xlink:href'
+  }
+}

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

@@ -0,0 +1,82 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const config = require('../config')
+const vueLoaderConfig = require('./vue-loader.conf')
+
+function resolve (dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+
+
+module.exports = {
+  context: path.resolve(__dirname, '../'),
+  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'],
+    alias: {
+      'vue$': 'vue/dist/vue.esm.js',
+      '@': resolve('src'),
+    }
+  },
+  module: {
+    rules: [
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader',
+        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('media/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+        }
+      }
+    ]
+  },
+  node: {
+    // prevent webpack from injecting useless setImmediate polyfill because Vue
+    // source contains it (although only uses it if it's native).
+    setImmediate: false,
+    // prevent webpack from injecting mocks to Node native modules
+    // that does not make sense for the client
+    dgram: 'empty',
+    fs: 'empty',
+    net: 'empty',
+    tls: 'empty',
+    child_process: 'empty'
+  }
+}

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

@@ -0,0 +1,95 @@
+'use strict'
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const path = require('path')
+const baseWebpackConfig = require('./webpack.base.conf')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
+const portfinder = require('portfinder')
+
+const HOST = process.env.HOST
+const PORT = process.env.PORT && Number(process.env.PORT)
+
+const devWebpackConfig = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
+  },
+  // cheap-module-eval-source-map is faster for development
+  devtool: config.dev.devtool,
+
+  // these devServer options should be customized in /config/index.js
+  devServer: {
+    clientLogLevel: 'warning',
+    historyApiFallback: {
+      rewrites: [
+        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
+      ],
+    },
+    hot: true,
+    contentBase: false, // since we use CopyWebpackPlugin.
+    compress: true,
+    host: HOST || config.dev.host,
+    port: PORT || config.dev.port,
+    open: config.dev.autoOpenBrowser,
+    overlay: config.dev.errorOverlay
+      ? { warnings: false, errors: true }
+      : false,
+    publicPath: config.dev.assetsPublicPath,
+    proxy: config.dev.proxyTable,
+    quiet: true, // necessary for FriendlyErrorsPlugin
+    watchOptions: {
+      poll: config.dev.poll,
+    }
+  },
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': require('../config/dev.env')
+    }),
+    new webpack.HotModuleReplacementPlugin(),
+    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
+    new webpack.NoEmitOnErrorsPlugin(),
+    // https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: 'index.html',
+      template: 'index.html',
+      inject: true
+    }),
+    // copy custom static assets
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: config.dev.assetsSubDirectory,
+        ignore: ['.*']
+      }
+    ])
+  ]
+})
+
+module.exports = new Promise((resolve, reject) => {
+  portfinder.basePort = process.env.PORT || config.dev.port
+  portfinder.getPort((err, port) => {
+    if (err) {
+      reject(err)
+    } else {
+      // publish the new Port, necessary for e2e tests
+      process.env.PORT = port
+      // add port to devServer config
+      devWebpackConfig.devServer.port = port
+
+      // Add FriendlyErrorsPlugin
+      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
+        compilationSuccessInfo: {
+          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
+        },
+        onErrors: config.dev.notifyOnErrors
+        ? utils.createNotifierCallback()
+        : undefined
+      }))
+
+      resolve(devWebpackConfig)
+    }
+  })
+})

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

@@ -0,0 +1,145 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const baseWebpackConfig = require('./webpack.base.conf')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
+const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
+
+const env = require('../config/prod.env')
+
+const webpackConfig = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.build.productionSourceMap,
+      extract: true,
+      usePostCSS: true
+    })
+  },
+  devtool: config.build.productionSourceMap ? config.build.devtool : 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 UglifyJsPlugin({
+      uglifyOptions: {
+        compress: {
+          warnings: false
+        }
+      },
+      sourceMap: config.build.productionSourceMap,
+      parallel: true
+    }),
+    // extract css into its own file
+    new ExtractTextPlugin({
+      filename: utils.assetsPath('css/[name].[contenthash].css'),
+      // Setting the following option to `false` will not extract CSS from codesplit chunks.
+      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
+      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 
+      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
+      allChunks: true,
+    }),
+    // Compress extracted CSS. We are using this plugin so that possible
+    // duplicated CSS from different components can be deduped.
+    new OptimizeCSSPlugin({
+      cssProcessorOptions: config.build.productionSourceMap
+        ? { safe: true, map: { inline: false } }
+        : { safe: true }
+    }),
+    // 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.html',
+      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'
+    }),
+    // keep module.id stable when vendor modules does not change
+    new webpack.HashedModuleIdsPlugin(),
+    // enable scope hoisting
+    new webpack.optimize.ModuleConcatenationPlugin(),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+      minChunks (module) {
+        // 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',
+      minChunks: Infinity
+    }),
+    // This instance extracts shared chunks from code splitted chunks and bundles them
+    // in a separate chunk, similar to the vendor chunk
+    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'app',
+      async: 'vendor-async',
+      children: true,
+      minChunks: 3
+    }),
+
+    // copy custom static assets
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: config.build.assetsSubDirectory,
+        ignore: ['.*']
+      }
+    ])
+  ]
+})
+
+if (config.build.productionGzip) {
+  const 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) {
+  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
+}
+
+module.exports = webpackConfig

+ 9 - 0
config/dev.env.js

@@ -0,0 +1,9 @@
+'use strict'
+const merge = require('webpack-merge')
+const prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+    NODE_ENV: '"development"',
+    // SSO_SERVER: '"http://192.168.20.101:8004"'
+    SSO_SERVER: '"http://sso.sagacloud.cn"'
+})

+ 75 - 0
config/index.js

@@ -0,0 +1,75 @@
+'use strict'
+// Template version: 1.3.1
+// see http://vuejs-templates.github.io/webpack for documentation.
+
+const path = require('path')
+
+module.exports = {
+  dev: {
+    // Paths
+    assetsSubDirectory: 'static',
+    assetsPublicPath: '/',
+    proxyTable: {
+      '/admin': {
+        target: 'http://localhost:52009',
+        changeOrigin: true,
+        pathRewrite: {
+          // "^/apis": ""
+        }
+      }
+    },
+
+    // Various Dev Server settings
+    host: 'localhost', // can be overwritten by process.env.HOST
+    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
+    autoOpenBrowser: false,
+    errorOverlay: true,
+    notifyOnErrors: true,
+    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
+
+    /**
+     * Source Maps
+     */
+
+    // https://webpack.js.org/configuration/devtool/#development
+    devtool: 'cheap-module-eval-source-map',
+
+    // If you have problems debugging vue-files in devtools,
+    // set this to false - it *may* help
+    // https://vue-loader.vuejs.org/en/options.html#cachebusting
+    cacheBusting: true,
+
+    cssSourceMap: true
+  },
+
+  build: {
+    // Template for index.html
+    index: path.resolve(__dirname, '../dist/index.html'),
+
+    // Paths
+    assetsRoot: path.resolve(__dirname, '../dist'),
+    assetsSubDirectory: 'static',
+    assetsPublicPath: '/',
+
+    /**
+     * Source Maps
+     */
+
+    productionSourceMap: true,
+    // https://webpack.js.org/configuration/devtool/#production
+    devtool: '#source-map',
+
+    // 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
+  }
+}

+ 5 - 0
config/prod.env.js

@@ -0,0 +1,5 @@
+'use strict'
+module.exports = {
+    NODE_ENV: '"production"',
+    SSO_SERVER: '"http://sso.sagacloud.cn"'
+}

+ 11 - 0
index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <title>sagacloud-admin</title>
+  </head>
+  <body>
+    <div id="app"></div>
+  </body>
+</html>

+ 72 - 0
package.json

@@ -0,0 +1,72 @@
+{
+  "name": "sagacloud-admin",
+  "version": "1.0.0",
+  "description": "A Vue.js project",
+  "author": "yaoll <yaolinlin@sagacloud.cn>",
+  "private": true,
+  "scripts": {
+    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
+    "start": "npm run dev",
+    "build": "node build/build.js"
+  },
+  "dependencies": {
+    "axios": "^0.18.0",
+    "echarts": "^4.1.0",
+    "element-ui": "^2.6.1",
+    "font-awesome": "^4.7.0",
+    "vue": "^2.5.2",
+    "vue-router": "^3.0.1",
+    "vuex": "^3.1.0"
+  },
+  "devDependencies": {
+    "ajv": "^6.9.1",
+    "autoprefixer": "^7.1.2",
+    "babel-core": "^6.22.1",
+    "babel-helper-vue-jsx-merge-props": "^2.0.3",
+    "babel-loader": "^7.1.1",
+    "babel-plugin-syntax-jsx": "^6.18.0",
+    "babel-plugin-transform-runtime": "^6.22.0",
+    "babel-plugin-transform-vue-jsx": "^3.5.0",
+    "babel-preset-env": "^1.3.2",
+    "babel-preset-stage-2": "^6.22.0",
+    "chalk": "^2.0.1",
+    "copy-webpack-plugin": "^4.0.1",
+    "css-loader": "^0.28.0",
+    "extract-text-webpack-plugin": "^3.0.0",
+    "file-loader": "^1.1.4",
+    "friendly-errors-webpack-plugin": "^1.6.1",
+    "html-webpack-plugin": "^2.30.1",
+    "less": "^3.9.0",
+    "less-loader": "^4.1.0",
+    "node-notifier": "^5.1.2",
+    "node-sass": "^4.11.0",
+    "optimize-css-assets-webpack-plugin": "^3.2.0",
+    "ora": "^1.2.0",
+    "portfinder": "^1.0.13",
+    "postcss-import": "^11.0.0",
+    "postcss-loader": "^2.0.8",
+    "postcss-url": "^7.2.1",
+    "rimraf": "^2.6.0",
+    "sass-loader": "^7.1.0",
+    "semver": "^5.3.0",
+    "shelljs": "^0.7.6",
+    "uglifyjs-webpack-plugin": "^1.1.1",
+    "url-loader": "^0.5.8",
+    "vue-loader": "^13.3.0",
+    "vue-style-loader": "^3.0.1",
+    "vue-template-compiler": "^2.5.2",
+    "webpack": "^3.6.0",
+    "webpack-bundle-analyzer": "^2.9.0",
+    "webpack-dev-server": "^2.9.1",
+    "webpack-merge": "^4.1.0"
+  },
+  "engines": {
+    "node": ">= 6.0.0",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ]
+}

+ 11 - 0
src/App.vue

@@ -0,0 +1,11 @@
+<template>
+    <div id='app'>
+        <router-view/>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'App'
+}
+</script>

+ 6 - 0
src/api/system/admin.js

@@ -0,0 +1,6 @@
+import httputils from '@/utils/httputils'
+export default {
+    loadUserInfo() {
+        return httputils.getJson('/admin/userInfo')
+    }
+}

BIN
src/assets/logo-small.jpg


BIN
src/assets/logo.jpg


+ 53 - 0
src/components/BasePagination.vue

@@ -0,0 +1,53 @@
+<template>
+    <el-pagination
+        @size-change='handleSizeChange'
+        @current-change='handleCurrentChange'
+        :current-page='currentPages?currentPages:pageInfo.currentPage'
+        :page-sizes='pageInfo.pageSizes'
+        :page-size='pageInfo.pageSize'
+        :layout='pageInfo.layout'
+        :total='pageInfo.total'
+        style='text-align: center'
+    ></el-pagination>
+</template>
+<script>
+export default {
+    name: 'base-pagination',
+    props: ['total', 'customconfig', 'currentPages'],
+    data() {
+        var vm = this
+        let pageInfo = this.initPagination()
+        if (!!this.total) {
+            pageInfo.total = this.total
+        }
+        //分页器可添加自定义的配置,在引用的时候,需要传递参数,如果有参数,就合并
+        if (!!this.customconfig) {
+            pageInfo = Object.assign(pageInfo, this.customconfig)
+        }
+        return {
+            pageInfo: pageInfo
+        }
+    },
+    methods: {
+        handleSizeChange: function(size) {
+            this.pageInfo.pageSize = size
+            this.$emit('pageChanged', this.pageInfo.currentPage, this.pageInfo.pageSize)
+        },
+        handleCurrentChange: function(currentPage) {
+            this.pageInfo.currentPage = currentPage
+            this.$emit('pageChanged', this.pageInfo.currentPage, this.pageInfo.pageSize)
+        }
+    },
+    watch: {
+        total: function() {
+            this.pageInfo.total = this.total
+        },
+        currentPages: {
+            immediate: true,
+            handler(val) {
+                this.pageInfo.currentPage = val
+            }
+        }
+    }
+}
+</script>

+ 401 - 0
src/data/menus.js

@@ -0,0 +1,401 @@
+export default {
+    demoMenus: [
+        {
+            icon: 'el-icon-fa-diamond',
+            name: 'Demo Page',
+            path: '/demo',
+            children: [
+                {
+                    icon: 'el-icon-fa-diamond',
+                    name: 'Form Panel',
+                    path: '/demo/form'
+                },
+                {
+                    icon: 'el-icon-fa-diamond',
+                    name: 'Table Panel',
+                    path: '/demo/table'
+                }
+            ]
+        }
+    ],
+    menus: [
+        {
+            path: '/system',
+            name: '系统管理',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/system/users',
+                    name: '用户管理',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:user:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            path: '/alarmconfig',
+            name: '报警设置',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/alarmconfig/projsetting',
+                    name: '项目报警设置',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/alarmconfig/projnotice',
+                    name: '项目报警通知设置',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/alarmconfig/staffsetting',
+                    name: '为断数报警设定通知人员',
+                    icon: 'el-icon-fa-key',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        // 与fm相关的配置
+        {
+            path: '/fm',
+            name: '与FM相关设置',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/fm/sopsetting',
+                    name: '通用问题与项目sop对应关系配置',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/fm/rulesetting',
+                    name: '项目计划抢单规则设定',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        // 与fm相关的配置
+        {
+            path: '/point',
+            name: '点位表管理',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/point/pointsetting',
+                    name: '点位表配置工具',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        // 项目化配置
+        {
+            path: '/proj',
+            name: '项目化配置',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/proj/custominfo',
+                    name: '自定义信息点',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/proj/collectsetting',
+                    name: '配置项目需采集的信息',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        //台账管理
+        {
+            path: '/ledger',
+            name: '台账管理',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/ledger/facility',
+                    name: '设备管理',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/ledger/system',
+                    name: '系统管理',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/ledger/property',
+                    name: '资产管理',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        //业务空间管理
+        {
+            path: '/business',
+            name: '业务空间管理',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/business/spacelist',
+                    name: '业务空间清单',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        //竖井管理
+        {
+            path: '/cenote',
+            name: '竖井管理',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/cenote/cenotelist',
+                    name: '竖井清单',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        //租户管理
+        {
+            path: '/rent',
+            name: '租户管理',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/rent/rentlist',
+                    name: '租户清单',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        //扫楼数据管理
+        {
+            path: '/floor',
+            name: '扫楼数据管理',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/floor/data',
+                    name: '扫楼数据管理',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/floor/plan',
+                    name: '楼层平面图管理',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/floor/abnormalprop',
+                    name: '异常资产清单',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/floor/log',
+                    name: '扫楼日志查看',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                },
+                {
+                    path: '/floor/appuser',
+                    name: '扫楼App用户管理',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        //环境调节
+        {
+            path: '/environment',
+            name: '环境调节',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/environment/envlist',
+                    name: '环境服务定制设置',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        //市场及商务
+        {
+            path: '/market',
+            name: '市场及商务',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/market/own',
+                    name: '业主管理',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+        //revit服务器端化的web功能
+        {
+            path: '/revitservice',
+            name: 'revit服务器端化',
+            icon: 'el-icon-fa-bank',
+            children: [
+                {
+                    path: '/revitservice/revit',
+                    name: '设计中',
+                    icon: 'el-icon-fa-user',
+                    opts: [
+                        {
+                            name: '查看',
+                            basic: true,
+                            permission: 'system:role:query'
+                        }
+                    ]
+                }
+            ]
+        },
+    ]
+}

+ 24 - 0
src/framework/layout/Login.vue

@@ -0,0 +1,24 @@
+<template>
+    <div>
+        <h4>Login</h4>
+    </div>
+</template>
+<script>
+export default {
+    name: 'Login',
+    props: [],
+    data() {
+        return {}
+    },
+    computed: {},
+    methods: {},
+    created() {
+        console.log('--------------------- Login created')
+        window.location = ''
+    },
+    mounted() {},
+    components: {}
+}
+</script>
+<style lang='scss' scoped>
+</style>

+ 64 - 0
src/framework/layout/Main.vue

@@ -0,0 +1,64 @@
+<template>
+    <div id='page-main' v-bind:class='{"page-sidebar-closed": sidebarClosed}'>
+        <page-header></page-header>
+        <div id='page-container' class='page-container'>
+            <page-sidebar></page-sidebar>
+
+            <div id='page-content-wrapper' class='page-content-wrapper'>
+                <div class='page-bar'>
+                    <el-breadcrumb separator='/'>
+                        <el-breadcrumb-item :to='{ path: "/" }'>首页</el-breadcrumb-item>
+                        <el-breadcrumb-item v-for='(b, index) in breadcrumb' :key='index'>{{b.label}}</el-breadcrumb-item>
+                    </el-breadcrumb>
+                </div>
+                <el-scrollbar tag="div" wrapClass="content-scrollbar">
+                    <router-view class='page-content'/>
+                </el-scrollbar>
+            </div>
+        </div>
+        <!-- <div class='page-footer'>footer</div> -->
+    </div>
+</template>
+<script>
+import { mapGetters, mapMutations } from 'vuex'
+import PageHeader from './PageHeader'
+import PageSidebar from './PageSidebar'
+export default {
+    name: 'Main',
+    components: {
+        'page-header': PageHeader,
+        'page-sidebar': PageSidebar
+    },
+    props: [],
+    data() {
+        return {}
+    },
+    computed: {
+        ...mapGetters('layout', ['sidebarClosed', 'breadcrumb'])
+    },
+    methods: {
+        ...mapMutations('layout', ['setSidebarClosed']),
+        windwoResize() {
+            // 窗口大小发生变化时
+            let clientWidth = `${document.documentElement.clientWidth}`
+            let clientHeight = `${document.documentElement.clientHeight}`
+            //this.setPageContentHeight(height)
+            if (clientWidth > 1000) {
+                this.setSidebarClosed(false)
+            } else if (clientWidth > 800) {
+                this.setSidebarClosed(true)
+            } else {
+                this.setSidebarClosed(false)
+            }
+        }
+    },
+    created() {
+        // console.log('--------------------- Main created')
+        // console.log(this.$store.getters.flag)
+    },
+    mounted() {
+        this.windwoResize()
+        window.onresize = this.windwoResize
+    }
+}
+</script>

+ 75 - 0
src/framework/layout/PageHeader.vue

@@ -0,0 +1,75 @@
+<template>
+    <div class='page-header'>
+
+        <div id='page-header-logo' class='page-logo'>
+            <a class='logo-big' href='/'>
+                <img src='@/assets/logo.jpg' alt='logo' class='logo-default'>
+            </a> 
+            <a class='logo-small' href='/'>
+                <img src='@/assets/logo-small.jpg' alt='logo' class='logo-default'>
+            </a>           
+        </div>
+        <div id='page-header-data-menu' class='data-menu'>
+            <div class='menu-toggler sidebar-toggler' @click.stop='toggleSidebar'>
+                <span>
+                    <i class='el-icon-fa-bars icon-arrows'></i>
+                </span>
+            </div>
+            <el-select v-model='selectedProjectId' placeholder='请选择'>
+                <el-option v-for='item in projects' :key='item.id' :label='item.name' :value='item.id'></el-option>
+            </el-select>
+        </div>
+        <div id='page-header-user-menu' class='user-menu'>
+            <el-dropdown trigger='hover' class='user-menu-dropdown' @command='userMenuCommand'>
+                <span class='el-dropdown-link'>
+                    {{userInfo.username}}
+                    <i class='el-icon-fa-user el-icon--right'></i>
+                </span>
+                <el-dropdown-menu slot='dropdown'>
+                    <el-dropdown-item icon='el-icon-plus' command='logout'>退出</el-dropdown-item>
+                    <el-dropdown-item icon='el-icon-circle-plus'>修改密码</el-dropdown-item>
+                </el-dropdown-menu>
+            </el-dropdown>
+        </div>
+    </div>
+</template>
+<script>
+import authutils from '@/utils/authutils'
+import { mapGetters, mapMutations } from 'vuex'
+export default {
+    name: 'PageHeader',
+    props: [],
+    data() {
+        return {
+            selectedProjectId: null
+        }
+    },
+    computed: {
+        ...mapGetters('layout', ['sidebarClosed', 'userInfo', 'projects', 'currentProjectId'])
+    },
+    watch: {
+        selectedProjectId(n, o) {
+            this.setCurrentProjectId(n)
+        }
+    },
+    methods: {
+        ...mapMutations('layout', ['setSidebarClosed', 'setCurrentProjectId']),
+        handleSelect() {},
+        toggleSidebar() {
+            this.setSidebarClosed(!this.sidebarClosed)
+        },
+        userMenuCommand(cmd) {
+            console.log('userMenuCommand ', cmd)
+            if (cmd == 'logout') {
+                authutils.toLogout()
+            }
+        }
+    },
+    created() {
+        // console.log('--------------------- PageHeader created')
+        this.selectedProjectId = this.currentProjectId
+    },
+    mounted() {},
+    components: {}
+}
+</script>

+ 77 - 0
src/framework/layout/PageSidebar.vue

@@ -0,0 +1,77 @@
+<template>
+    <div class='page-sidebar-wrapper'>
+        <el-menu
+            :collapse='sidebarClosed'
+            :default-active='sidebarSelected'
+            @open='handleOpen'
+            @close='handleClose'
+            @select='menuSelect'
+            text-color='#333333'
+            active-text-color='#5BA7FF'
+            unique-opened
+            router
+        >
+            <template v-for='menu in menus'>
+                <!-- <div class='underline-box'> -->
+                    <el-submenu v-if='menu.children' :index='menu.path' :key='menu.path'>
+                        <template slot='title'>
+                            <i v-if='menu.icon' :class='"el-icon-fa "+ menu.icon'></i>
+                            <span>{{menu.name}}</span>
+                        </template>
+                        <template v-for='submenu in menu.children'>
+                                <el-menu-item :index='submenu.path' :key='submenu.path'>
+                                    <i v-if='submenu.icon' :class='"el-icon-fa "+ submenu.icon'></i>
+                                    <span slot='title'>{{submenu.name}}</span>
+                                </el-menu-item>
+                        </template>
+                    </el-submenu>
+                    <el-menu-item v-else :index='menu.path' :key='menu.path'>
+                        <i v-if='menu.icon' :class='"el-icon-fa "+ menu.icon'></i>
+                        <span slot='title'>{{menu.name}}</span>
+                    </el-menu-item>
+                    <!-- <p class='underline'></p> -->
+                <!-- </div> -->
+            </template>
+        </el-menu>
+    </div>
+</template>
+<script>
+import { mapGetters, mapMutations } from 'vuex'
+import authutils from '@/utils/authutils'
+export default {
+    name: 'PageSidebar',
+    props: [],
+    data() {
+        return {}
+    },
+    computed: {
+        ...mapGetters('layout', ['sidebarClosed', 'sidebarSelected', 'permissions']),
+        menus() {
+            // console.log('*******************')
+            // console.log(this.permissions)
+            let menus = authutils.getMenus(this.permissions)
+            // console.log('sidebar menus ', menus)
+            return menus
+        }
+    },
+    methods: {
+        ...mapMutations('layout', ['setSidebarSelected']),
+        handleOpen(val) {
+            // console.log('handleOpen------------- ', val)
+        },
+        handleClose(val) {
+            // console.log('handleClose------------- ', val)
+        },
+        menuSelect(index, indexPath) {
+            this.setSidebarSelected(index)
+            // console.log('menu select ', index, indexPath)
+        }
+    },
+    created() {
+        // console.log('--------------------- PageSidebar created')
+        // console.log('menus ', this.menus)
+    },
+    mounted() {},
+    components: {}
+}
+</script>

+ 140 - 0
src/framework/layout/layout-store.js

@@ -0,0 +1,140 @@
+import adminapi from '@/api/system/admin'
+import storage from '@/utils/storage'
+
+const KEY_MENU_SELECTED = 'menu_selected'
+const KEY_PROJECT_SELECTED = 'global_project_selected'
+const KEY_PAGE_BRANDCRUMB = 'page_brandcrumb'
+
+export default {
+    namespaced: true,
+    state: {
+        sidebarClosed: false,
+        sidebarSelected: '', // sidebar选中的选项
+        userInfo: { username: 'admin' },
+        permissions: {"system:role:delete": true, "system:role:create": true, "system:role:query": true, "system:role:setOpts": true},
+        currentProjectId: null,
+        projects: [],
+        breadcrumb: []
+    },
+    getters: {
+        sidebarClosed: state => state.sidebarClosed,
+        sidebarSelected: state => {
+            if (!state.pageSidebarSelected) {
+                let menu = storage.get(KEY_MENU_SELECTED)
+                if (menu) {
+                    state.pageSidebarSelected = menu
+                }
+            }
+            return state.pageSidebarSelected
+        },
+        userInfo: state => state.userInfo,
+        permissions: state => state.permissions,
+        projects: state => state.projects,
+        currentProjectId: state => {
+            if (!state.currentProjectId) {
+                let pid = storage.get(KEY_PROJECT_SELECTED)
+                if (pid) {
+                    state.currentProjectId = pid
+                }
+            }
+            return state.currentProjectId
+        },
+        breadcrumb: state => {
+            if (!state.breadcrumb) {
+                let arr = storage.get(KEY_PAGE_BRANDCRUMB)
+                if (arr) {
+                    state.breadcrumb = arr
+                }
+            }
+            return state.breadcrumb
+        }
+    },
+    mutations: {
+        setSidebarClosed: (state, val) => (state.sidebarClosed = val),
+        setSidebarSelected: (state, val) => {
+            state.sidebarSelected = val
+            storage.set(KEY_MENU_SELECTED, val)
+        },
+        setCurrentProjectId: (state, val) => {
+            state.currentProjectId = val
+            storage.set(KEY_PROJECT_SELECTED, val)
+        }
+    },
+    actions: {
+        loadUserInfo({ state }) {
+            return new Promise((resolve, reject) => {
+                adminapi.loadUserInfo().then(resp => {
+                    console.log('api load user info ', resp)
+                    if (resp.result == 'success') {
+                        state.userInfo = { username: resp.username }
+                        state.permissions = {}
+                        if (resp.permissions) {
+                            resp.permissions.forEach(p => (state.permissions[p] = true))
+                        }
+                        state.projects = []
+                        if (resp.projects) {
+                            resp.projects.forEach(proj =>
+                                state.projects.push({
+                                    id: proj.projId,
+                                    name: proj.projLocalName
+                                })
+                            )
+                        }
+                    } else {
+                        state.userInfo = null
+                    }
+                    resolve(resp)
+                })
+                // let resp = {
+                //     result: 'success',
+                //     message: null,
+                //     projects: [
+                //         {
+                //             projId: 'Pj4419000006',
+                //             projName: '广东省东莞市市辖区0001项目',
+                //             projLocalName: '厚街万科城市广场',
+                //             longitude: '113.23',
+                //             latitude: '23.16',
+                //             distance: 500
+                //         }
+                //     ],
+                //     permissions: [
+                //         'system:role:delete',
+                //         'system:role:create',
+                //         'system:role:query',
+                //         'system:role:setOpts',
+                //         'system:role:update'
+                //     ],
+                //     username: 'fuguoyu'
+                // }
+                // if (resp.result == 'success') {
+                //     state.userInfo = { username: resp.username }
+                //     state.permissions = {}
+                //     if (resp.permissions) {
+                //         resp.permissions.forEach(p => (state.permissions[p] = true))
+                //     }
+                //     state.projects = []
+                //     if (resp.projects) {
+                //         resp.projects.forEach(proj =>
+                //             state.projects.push({
+                //                 id: proj.projId,
+                //                 name: proj.projLocalName
+                //             })
+                //         )
+                //     }
+                // } else {
+                //     state.userInfo = null
+                // }
+                // resolve(resp)
+            })
+        },
+        setBreadcrumb: {
+            root: true,
+            handler({ state }, val) {
+                state.breadcrumb = []
+                state.breadcrumb = val
+                storage.set(KEY_PAGE_BRANDCRUMB, val)
+            }
+        }
+    }
+}

+ 18 - 0
src/framework/style/awesome.less

@@ -0,0 +1,18 @@
+[class^='el-icon-fa'],
+[class*=' el-icon-fa'] {
+  font-family: FontAwesome !important;
+  font-size: inherit;
+  speak: none;
+  font-style: normal;
+  font-weight: 400;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+  vertical-align: baseline;
+  display: inline-block;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+@import url('../../../node_modules/font-awesome/less/font-awesome');
+@fa-css-prefix: el-icon-fa;

+ 346 - 0
src/framework/style/layout.scss

@@ -0,0 +1,346 @@
+@import './theme.scss';
+
+$pageHeaderHeight: 60px;
+
+$pageSidebarWidth: 220px;
+$pageSidebarClosedWidth: 60px;
+
+$pageFooterHeight: 40px;
+
+html,
+body {
+    padding: 0 !important;
+    margin: 0 !important;
+    font-size: 14px;
+    width: 100%;
+    height: 100%;
+}
+#app {
+    width: 100%;
+    height: 100%;
+}
+#page-main {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column; /* 垂直 从上到下 */
+    justify-content: flex-start; /* 垂直 左对齐 */
+    align-items: stretch;
+    .page-header {
+        order: 0;
+        flex-grow: 0;
+        flex-shrink: 0;
+        height: $pageHeaderHeight;
+    }
+
+    .page-container {
+        order: 1;
+        flex-grow: 1;
+        flex-shrink: 1;
+
+        display: flex;
+        flex-direction: row; /* 水平,从左到右 */
+        justify-content: flex-start; /* 水平上对齐 */
+
+        .page-sidebar-wrapper {
+            width: $pageSidebarWidth;
+            overflow-y: auto;
+            flex-grow: 0;
+            flex-shrink: 0;
+            overflow: hidden;
+            border-right: 1px solid #c2cedb;
+            // .underline-box {
+            //     position: relative;
+            //     overflow: hidden;
+            //     .underline {
+            //         position: absolute;
+            //         width: 180px;
+            //         height: 1px;
+            //         background-color: #C2CEDB;
+            //         left: 20px;
+            //         bottom: -14px;
+            //     }
+            // }
+        }
+        .page-content-wrapper {
+            flex-grow: 1;
+            flex-shrink: 1;
+            .content-scrollbar {
+                height: calc(100vh - 108px);
+                .el-scrollbar__bar {
+                    &.is-vertical {
+                        display: none;
+                    }
+                }
+            }
+        }
+    }
+
+    .page-footer {
+        order: 2;
+        flex-grow: 0;
+        flex-shrink: 0;
+        height: $pageFooterHeight;
+    }
+}
+
+#page-main .page-header {
+    display: flex;
+    flex-direction: row; /* 水平, 从左向右 */
+    justify-content: flex-start; /* 垂直 左对齐 */
+
+    .page-logo {
+        flex-grow: 0;
+        flex-shrink: 0;
+
+        width: 220px;
+
+        display: inline-flex;
+        flex-direction: row;
+        justify-content: space-between;
+        > .logo-big {
+            display: inline-block;
+            margin-top: 14px;
+            margin-left: 66px;
+        }
+        > .logo-small {
+            display: none;
+        }
+    }
+    .data-menu {
+        flex-grow: 0;
+        flex-shrink: 1;
+        width: 290px;
+        height: 100%;
+        line-height: $pageHeaderHeight;
+        display: flex;
+        .menu-toggler {
+            margin-right: 10px;
+            .icon-arrows {
+                display: inline-block;
+                width: 24px;
+                height: 24px;
+                margin-left: 10px;
+                line-height: 24px;
+                text-align: center;
+                font-size: 20px;
+            }
+        }
+        .data-menu-dropdown {
+            height: 100%;
+            line-height: $pageHeaderHeight;
+            font-size: 18px;
+        }
+        .el-select .el-input--suffix .el-input__inner {
+            height: 30px;
+            line-height: 30px;
+        }
+    }
+    .user-menu {
+        flex-grow: 1;
+        flex-shrink: 1;
+        margin: 0 20px 0 0;
+        padding: 0;
+        height: 100%;
+        display: inline-flex;
+        flex-direction: row-reverse;
+        .user-menu-dropdown {
+            height: 100%;
+            line-height: $pageHeaderHeight;
+            font-size: 18px;
+            margin-left: 20px;
+        }
+    }
+}
+
+#page-main .page-container {
+    .page-sidebar-wrapper {
+        .el-menu {
+            border-right: none;
+            .el-submenu__title,
+            .el-menu-item {
+                height: 40px;
+                line-height: 40px;
+            }
+            .el-menu--inline {
+                background: #eefaff;
+                border-left: 4px solid #5ba7ff;
+            }
+        }
+    }
+}
+
+#page-main .page-container .page-content-wrapper {
+    margin: 0px 20px 10px 10px;
+
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-start;
+
+    .page-bar {
+        flex-grow: 0;
+        flex-shrink: 0;
+        height: 30px;
+        padding: 0 20px;
+        margin-bottom: 10px;
+        border-bottom: 1px solid #c2cedb;
+        .el-breadcrumb {
+            font-size: 14px;
+            line-height: 30px;
+        }
+    }
+    .page-content {
+        flex-grow: 1;
+        flex-shrink: 1;
+        overflow-y: auto;
+    }
+}
+
+#page-main #page-container #page-content-wrapper .page-content.page-table-template {
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-start;
+
+    .query-form {
+        flex-grow: 0;
+        flex-shrink: 0;
+    }
+    .data-table {
+        flex-grow: 1;
+        flex-shrink: 1;
+        overflow-y: auto;
+    }
+    .data-table-pagination {
+        flex-grow: 0;
+        flex-shrink: 0;
+    }
+}
+
+#page-main #page-container #page-content-wrapper .page-content .query-form {
+    padding: 5px 10px 0px 10px;
+    margin-bottom: 10px;
+    .el-form {
+        .el-form-item {
+            margin-bottom: 5px;
+            vertical-align: middle;
+            .el-form-item__label {
+                //min-width: 72px;
+            }
+            .el-form-item__content {
+                margin-left: 0px;
+            }
+        }
+        .halving_line {
+            width: 100%;
+            border-top: 1px solid #dfe6ec;
+        }
+    }
+}
+
+#page-main #page-container #page-content-wrapper .page-content .data-table {
+    .el-table__header-wrapper {
+        .el-table__header {
+            thead tr th {
+            }
+        }
+    }
+    .el-table__body-wrapper {
+        .el-table__body {
+            tr td {
+                padding: 6px 0;
+                .cell {
+                    line-height: 20px;
+                }
+            }
+        }
+    }
+}
+
+#page-main #page-container #page-content-wrapper .page-content.input-form {
+    form {
+        max-width: 800px;
+        margin: 50px auto;
+
+        > .el-form-item {
+            margin-bottom: 18px;
+
+            > .el-form-item__label {
+                width: 180px;
+            }
+
+            > .el-form-item__content {
+                max-width: 600px;
+                margin-left: 180px;
+            }
+        }
+    }
+}
+
+#page-main.page-sidebar-closed {
+    .page-header {
+        .page-logo {
+            padding-left: 20px;
+            padding-right: 20px;
+            width: 20px;
+            > .logo-big {
+                display: none;
+            }
+            > .logo-small {
+                display: inline-block;
+                margin-top: 14px;
+            }
+        }
+    }
+    .page-container {
+        .page-sidebar-wrapper {
+            width: $pageSidebarClosedWidth;
+        }
+    }
+}
+
+@media screen and (max-width: 1000px) {
+    #page-main {
+        .page-header {
+            .page-logo {
+                padding-left: 20px;
+                padding-right: 20px;
+                width: 20px;
+                > a {
+                    display: none;
+                }
+            }
+        }
+        .page-container {
+            .page-sidebar-wrapper {
+                width: $pageSidebarClosedWidth;
+            }
+        }
+    }
+}
+
+@media screen and (max-width: 800px) {
+    #page-main {
+        .page-header {
+            .page-logo {
+                display: none;
+            }
+        }
+        .page-container {
+            display: flex;
+            flex-direction: column; /* 垂直,从上到下 */
+            justify-content: flex-start; /* 垂直 左对齐 */
+
+            .page-sidebar-wrapper {
+                width: 100%;
+            }
+        }
+    }
+}
+
+@media screen and (max-height: 700px) {
+    #page-main {
+        .page-footer {
+            display: none;
+        }
+    }
+}

+ 65 - 0
src/framework/style/theme.scss

@@ -0,0 +1,65 @@
+$themeColor: #5BA7FF;
+.body {
+    // color: #333;
+}
+#app {
+    .page-header {
+        background: $themeColor;
+
+        .page-logo {
+            background: #fff;
+        }
+
+        .data-menu {
+            .data-menu-dropdown {
+                color: #fff;
+            }
+            .icon-arrows {
+                background: #fff;
+                color: $themeColor;
+            }
+            // .el-select .el-input--suffix .el-input__inner {
+            //     background: #e3e4e6;
+            // }
+        }
+        .user-menu {
+            .user-menu-dropdown {
+                color: #fff;
+            }
+        }
+    }
+
+    .page-container {
+        background: #f2f2f2;
+
+        .page-sidebar-wrapper {
+            // color: azure;
+            background: #fff;
+            // .el-menu {
+            //     background: #364150;
+            // }
+        }
+        .page-content-wrapper {
+            .page-content {
+                .query-form {
+                    border: 1px solid #dfe6ec;
+                    background-color: #fff;
+                }
+                .data-table {
+                    .el-table__header-wrapper {
+                        .el-table__header {
+                            thead tr th {
+                                background-color: #d9d9d9;
+                                // color: #000;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    .page-footer {
+        background: #28303b;
+    }
+}

+ 32 - 0
src/framework/template/TablePageTemplate.vue

@@ -0,0 +1,32 @@
+<template>
+    <div class='page-table-template'>
+        <div class='query-form'>
+            <slot name='form'></slot>
+        </div>
+        <div class='data-table'>
+            <slot name='table'></slot>
+        </div>
+        <div class='data-table-pagination'>
+            <slot name='pagination'></slot>
+        </div>
+        <slot name='dialog'></slot>
+    </div>
+</template>
+<script>
+export default {
+    name: 'TablePageTemplate',
+    props: [],
+    data() {
+        return {}
+    },
+    computed: {},
+    methods: {},
+    created() {
+        console.log('--------------------- TablePageTemplate created')
+    },
+    mounted() {},
+    components: {}
+}
+</script>
+<style lang='scss' scoped>
+</style>

+ 27 - 0
src/main.js

@@ -0,0 +1,27 @@
+// The Vue build version to load with the `import` command
+// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
+import Vue from 'vue'
+import App from './App'
+import router from './router'
+import store from './store'
+
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+import './framework/style/awesome.less'
+Vue.use(ElementUI)
+
+import './framework/style/layout.scss'
+
+import componentsPlugin from '@/plugins/components'
+Vue.use(componentsPlugin)
+
+Vue.config.productionTip = false
+
+/* eslint-disable no-new */
+new Vue({
+  el: '#app',
+  router,
+  store,
+  components: { App },
+  template: '<App/>'
+})

+ 40 - 0
src/plugins/components.js

@@ -0,0 +1,40 @@
+import BasePagination from '@/components/BasePagination'
+import TableTemplate from '@/framework/template/TablePageTemplate'
+//tools
+import basicutils from '@/utils/basicutils'
+
+export default {
+  install: function(Vue) {
+    Vue.prototype.initPagination = function() {
+      return {
+        pageSize: 10,
+        pageSizes: [10, 20, 50, 100, 200, 500],
+        layout: 'total, sizes, prev, pager, next, jumper',
+        total: 0,
+        currentPage: 1,
+        data: [],
+        params: {}
+      }
+    }
+    Vue.component('base-pagination', BasePagination)
+    Vue.component('table-page-template', TableTemplate)
+
+    Vue.prototype.formatDate = function(date) {
+      return basicutils.formatDateByPattern(date, 'yyyy-MM-dd HH:mm:ss')
+    }
+
+    Vue.prototype.hasPermission = function(promise) {
+      return basicutils.hasPermission(promise)
+    }
+
+    Vue.filter('formatDateForMillisecond', function(value) {
+      if (!value) return ''
+      return basicutils.formatDateByPattern(new Date(value), 'yyyy-MM-dd')
+    })
+
+    Vue.filter('formatDateTimeForMillisecond', function(value) {
+      if (!value) return ''
+      return basicutils.formatDateByPattern(new Date(value), 'yyyy-MM-dd HH:mm:ss')
+    })
+  }
+}

+ 17 - 0
src/router/demo.js

@@ -0,0 +1,17 @@
+import LayoutMain from '@/framework/layout/Main'
+
+import TablePageDemo from '@/views/demo/TablePageDemo'
+import FormPageDemo from '@/views/demo/FormPageDemo'
+
+export default [
+  {
+    path: '/demo',
+    name: 'LayoutMain',
+    component: LayoutMain,
+    children: [
+        { path: '', name: '', component: TablePageDemo },
+      { path: 'table', name: 'TablePageDemo', component: TablePageDemo },
+      { path: 'form', name: 'FormPageDemo', component: FormPageDemo }
+    ]
+  }
+]

+ 19 - 0
src/router/index.js

@@ -0,0 +1,19 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import authutils from '@/utils/authutils'
+import demoRoute from './demo'
+import sagacloudRoute from './sagacloud'
+Vue.use(Router)
+
+let routes = []
+routes = routes.concat(demoRoute)
+routes = routes.concat(sagacloudRoute)
+
+const router = new Router({
+  mode: 'history',
+  routes: routes
+})
+
+router.beforeEach(authutils.routerBeforeEach)
+
+export default router

+ 149 - 0
src/router/sagacloud.js

@@ -0,0 +1,149 @@
+import LayoutMain from '@/framework/layout/Main'
+
+import Auth from '@/views/system/auth'
+import Dasboard from '@/views/dasboard'
+import ChangePwd from '@/views/system/pwd/ChangePwd'
+
+export default [
+    { path: '/auth', name: 'Auth', component: Auth },
+    // 系统相关
+    {
+        path: '/',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: '/system/changepwd', name: 'ChangePwd', component: ChangePwd }
+        ]
+    },
+    // 报警设置
+    {
+        path: '/alarmconfig',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'projsetting', name: 'Dasboard', component: Dasboard },
+            { path: 'projnotice', name: 'Dasboard', component: Dasboard },
+            { path: 'staffsetting', name: 'Dasboard', component: Dasboard }
+        ]
+    },
+    // 与FM相关设置
+    {
+        path: '/fm',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'sopsetting', name: 'Dasboard', component: Dasboard },
+            { path: 'rulesetting', name: 'Dasboard', component: Dasboard }
+        ]
+    },
+    // 点位表管理
+    {
+        path: '/point',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'pointsetting', name: 'Dasboard', component: Dasboard }
+        ]
+    },
+    //项目化配置
+    {
+        path: '/proj',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'custominfo', name: 'Dasboard', component: Dasboard },
+            { path: 'collectsetting', name: 'Dasboard', component: Dasboard }
+        ]
+    },
+    //台账管理
+    {
+        path: '/ledger',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'facility', name: 'Dasboard', component: Dasboard },
+            { path: 'system', name: 'Dasboard', component: Dasboard },
+             { path: 'property', name: 'Dasboard', component: Dasboard }
+        ]
+    },
+    //业务空间管理    
+    {
+        path: '/business',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'spacelist', name: 'Dasboard', component: Dasboard }
+        ]
+    },
+    //竖井管理
+    {
+        path: '/cenote',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'cenotelist', name: 'Dasboard', component: Dasboard }
+        ]
+    },
+    //租户管理
+    {
+        path: '/rent',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'rentlist', name: 'Dasboard', component: Dasboard }
+        ]
+    },
+    //扫楼数据管理
+    {
+        path: '/floor',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'data', name: 'Dasboard', component: Dasboard },
+            { path: 'plan', name: 'Dasboard', component: Dasboard },
+            { path: 'abnormalprop', name: 'Dasboard', component: Dasboard },
+            { path: 'log', name: 'Dasboard', component: Dasboard },
+            { path: 'appuser', name: 'Dasboard', component: Dasboard },
+        ]
+    },
+    //环境调节
+    {
+        path: '/environment',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'envlist', name: 'Dasboard', component: Dasboard }            
+        ]
+    },
+    //市场及商务
+    {
+        path: '/market',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'own', name: 'Dasboard', component: Dasboard }            
+        ]
+    },
+    //revit服务器端化的web功能
+    {
+        path: '/revitservice',
+        name: 'LayoutMain',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'Dasboard', component: Dasboard },
+            { path: 'revit', name: 'Dasboard', component: Dasboard }            
+        ]
+    },
+]

+ 39 - 0
src/store/index.js

@@ -0,0 +1,39 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import storage from '@/utils/storage'
+import layout from '@/framework/layout/layout-store'
+Vue.use(Vuex)
+
+const KEY_LAST_ROUTE = 'last_route'
+
+export default new Vuex.Store({
+    state: {
+        flag: 'sagacloud-admin',
+        ssoToken: null,
+        lastRoute: null
+    },
+    getters: {
+        flag: state => state.flag,
+        ssoToken: state => state.ssoToken,
+        lastRoute: state => {
+            if (!state.lastRoute) {
+                let lastRoute = storage.get(KEY_LAST_ROUTE)
+                if (lastRoute) {
+                    state.lastRoute = lastRoute
+                }
+            }
+            return state.lastRoute
+        }
+    },
+    mutations: {
+        setSsoToken: (state, val) => (state.ssoToken = val),
+        setLastRoute: (state, val) => {
+            state.lastRoute = val
+            storage.set(KEY_LAST_ROUTE, val)
+        }
+    },
+    actions: {},
+    modules: {
+        layout
+    }
+})

+ 118 - 0
src/utils/authutils.js

@@ -0,0 +1,118 @@
+import httputils from '@/utils/httputils'
+import store from '@/store'
+import menus from '@/data/menus'
+
+function toLogin() {
+    let ssoServer = process.env.SSO_SERVER
+    let redirectUrl = window.location.protocol + '//' + window.location.host
+    window.location.href = `${ssoServer}/login?redirectUrl=${redirectUrl}/auth`
+}
+
+function checkMenu(menu, ps) {
+    let result = { name: menu.name, icon: menu.icon, path: menu.path }
+    if (menu.children) {
+        // 如果有下级菜单权限,则自动拥有上级菜单权限
+        result.children = []
+        menu.children.forEach(child => {
+            let submenu = checkMenu(child, ps)
+            if (submenu) {
+                result.children.push(submenu)
+            }
+        })
+        return result.children.length > 0 ? result : null
+    } else if (menu.opts) {
+        return menu.opts.some(opt => ps[opt.permission]) ? result : null
+    } else {
+        // 如果没有下级菜单且没有opts属性, 菜单可以直接访问,不需要权限
+        return result
+    }
+}
+
+export default {
+    /**
+     *  路由守卫, 每次路由跳转时验证登录
+     * @param {*} to
+     * @param {*} from
+     * @param {*} next
+     */
+    routerBeforeEach: async function(to, from, next) {
+        // console.log('router before ', to)
+        if (to.path == '/auth') {
+            next()
+        } else {           
+            // let userInfo = store.getters['layout/userInfo']
+            // if (!userInfo) {
+            //     // 本地是未登录状态, 保存目标页面地址, 去登录
+            //     let lastRoute = { path: to.path, params: to.params, query: to.query }
+            //     store.commit('setLastRoute', lastRoute)
+            //     toLogin()
+            // } else {
+            //     next()
+            // }
+            next()
+            return true
+        }
+    },
+
+    // routerBeforeEach2: async function(to, from, next) {
+    //     // console.log('router before ', to)
+    //     if (to.path == '/login') {
+    //         next()
+    //         return false
+    //     }
+    //     let userInfo = store.getters['layout/userInfo']
+    //     // console.log('user info ', userInfo)
+    //     if (!userInfo) {
+    //         // 本地是未登录状态, 检查是否有sso令牌
+    //         var token = httputils.getCookie('sagacloud-auth-token')
+    //         // console.log('sso token ', token)
+    //         if (!token) {
+    //             // 没有sso令牌, 跳转登录页面
+    //             toLogin()
+    //             return false
+    //         } else {
+    //             // 有sso令牌, 去服务端校验令牌并获取用户信息
+    //             // next({ path: to.path, params: to.params, query: to.query })
+    //             store.dispatch('layout/loadUserInfo').then(resp => {
+    //                 // console.log('store dispatch result ', resp)
+    //                 if (resp.result == 'success') {
+    //                     next({ path: to.path, params: to.params, query: to.query })
+    //                 } else {
+    //                     toLogin()
+    //                 }
+    //             })
+    //         }
+    //     } else {
+    //         next()
+    //     }
+    //     return true
+    // },
+
+    getMenus(permissions) {
+        let result = []
+        let allMenus = []
+        // 开发环境下展示demo页面
+        if (process.env.NODE_ENV === 'development') {
+            allMenus = menus.demoMenus
+        }
+        allMenus = allMenus.concat(menus.menus)
+        let ps = !permissions ? {} : permissions
+        allMenus.forEach(item => {
+            let menu = checkMenu(item, ps)
+            if (menu) {
+                result.push(menu)
+            }
+        })
+        return result
+    },
+
+    toLogout() {
+        // TODO
+        store.commit('setSsoToken', null)
+        let ssoServer = process.env.SSO_SERVER
+        let redirectUrl = window.location.protocol + '//' + window.location.host + '/'
+        window.location.href = `${ssoServer}/logout?redirectUrl=${redirectUrl}`
+    },
+
+    toLoginPage: toLogin
+}

+ 102 - 0
src/utils/basicutils.js

@@ -0,0 +1,102 @@
+import store from '@/store';
+function deepCopy(source) {
+    if (!source) {
+        return null
+    }
+    var result
+    source instanceof Array ? (result = []) : (result = {})
+    for (var key in source) {
+        result[key] = typeof source[key] === 'object' ? deepCopy(source[key]) : source[key]
+    }
+    return result
+}
+
+export default {
+    formatDateByPattern: function(date, pattern) {
+        let fmt = pattern
+        var o = {
+            'M+': date.getMonth() + 1, //月份
+            'd+': date.getDate(), //日
+            'H+': date.getHours(), //小时
+            'm+': date.getMinutes(), //分
+            's+': date.getSeconds(), //秒
+            'q+': Math.floor((date.getMonth() + 3) / 3), //季度
+            S: date.getMilliseconds() //毫秒
+        }
+        if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
+        for (var k in o)
+            if (new RegExp('(' + k + ')').test(fmt))
+                fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
+        return fmt
+    },
+
+    deepCopy: deepCopy,
+
+    /**
+     * 找出数组arr中第一个属性key的值等于val的元素的下标
+     * @param {*} arr 数组
+     * @param {*} key 属性
+     * @param {*} val 值
+     */
+    indexInArray: function(arr, key, val) {
+        if (!arr || !arr.length || arr.length == 0) {
+            return -1
+        } else {
+            for (let index = 0; index < arr.length; index++) {
+                const element = arr[index]
+                if (key in element && element[key] == val) {
+                    return index
+                }
+            }
+        }
+        return -1
+    },
+
+    /**
+     * 找出数组arr中第一个属性key的值等于val的元素
+     * @param {*} arr 数组
+     * @param {*} key 属性名
+     * @param {*} val 属性值
+     */
+    itemInArray: function(arr, key, val) {
+        for (let index = 0; index < arr.length; index++) {
+            const element = arr[index]
+            if (key in element && element[key] == val) {
+                return element
+            }
+        }
+        return null
+    },
+
+    /**
+     * 从数组中将属性key的值等于val的元素删除
+     * @param {*} arr 数组
+     * @param {*} key 属性名
+     * @param {*} val 属性值
+     */
+    deleteInArray: function(arr, key, val) {
+        if (!arr || !arr.length || arr.length == 0) {
+            return
+        } else {
+            for (let index = arr.length - 1; index > -1; index--) {
+                const element = arr[index]
+                if (element[key] && element[key] == val) {
+                    arr.splice(index, 1)
+                }
+            }
+        }
+    },
+    /**
+     * 判断该用户的权限是否具有某些权限
+     * @param {*}  promise 字符串
+     * @return ture | fasle
+     */
+    hasPermission: function(promise) {
+        let permissions = store.getters['layout/permissions']
+        if(permissions[promise]) {
+            return true;
+        } else {
+            return false
+        }
+    }
+}

+ 106 - 0
src/utils/httputils.js

@@ -0,0 +1,106 @@
+import axios from 'axios'
+import store from '@/store'
+import { MessageBox } from 'element-ui'
+
+var CancelToken = axios.CancelToken
+var cancel
+
+// 创建axios实例
+const axiosservice = axios.create({
+    timeout: 30000, // 请求超时时间
+    retry: 4, //重新请求次数
+    retryDelay: 1000, //重新请求的间隔
+    credentials: true, // 接口每次请求会跨域携带cookie
+    cancelToken: new CancelToken(function executor(c) {
+        // executor 函数接收一个 cancel 函数作为参数
+        cancel = c
+    })
+})
+
+axiosservice.interceptors.request.use(
+    config => {
+        config.withCredentials = true // 允许携带token ,这个是解决跨域产生的相关问题
+        let token = store.getters['ssoToken']
+        if (token) {
+            config.headers = {
+                'sso-token': token
+            }
+        }
+        return config
+    },
+    error => {
+        return Promise.reject(error)
+    }
+)
+
+axiosservice.interceptors.response.use(
+    function(res) {
+        //在这里对返回的数据进行处理
+        //console.log('axios interceptors res = ', res.status, res)
+        let resp = res.data
+        if (resp.result === 'unauthc') {
+            store.commit('logined', false)
+            MessageBox.confirm('未登陆或登陆信息已失效, 是否重新登陆?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'error'
+            })
+                .then(resp => {
+                    //console.log('--------------------------- confirm', resp)
+                    //router.push('/login')
+                    window.location.reload()
+                })
+                .catch(error => {
+                    //console.log('--------------------------- cancel', error)
+                    console.log('')
+                })
+        }
+        //console.log('axios interceptors resp2 = ', resp.success, resp.errorCode, resp.errorMessage, res)
+        return res
+    },
+    function(err) {
+        //Do something with response error
+        console.log('axios interceptors err = ', err)
+        return Promise.reject(err)
+    }
+)
+
+export default {
+    //获取cookie
+    getCookie(name) {
+        var arr,
+            reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)')
+        if ((arr = document.cookie.match(reg))) {
+            return unescape(arr[2])
+        } else {
+            /* 如果没有参数,那么就获取本域下的所有cookie */
+            return document.cookie
+        }
+    },
+
+    async getJson(url, params) {
+        try {
+            let response = await axiosservice({
+                url,
+                params,
+                method: 'get'
+            })
+            return response.data
+        } catch (err) {
+            throw err
+        }
+    },
+    async postJson(url, data) {
+        try {
+            let response = await axiosservice({
+                url,
+                data,
+                method: 'post'
+            })
+            return response.data
+        } catch (err) {
+            throw err
+        }
+    },
+    axios: axiosservice
+}

+ 33 - 0
src/utils/storage.js

@@ -0,0 +1,33 @@
+const PREFIX = '_sagacloud_admin_store_'
+const sessionStorage = window.sessionStorage
+
+export default {
+    set(key, value, fn) {
+        let _value = null
+        try {
+            _value = JSON.stringify(value)
+        } catch (e) {
+            _value = value
+        }
+        sessionStorage.setItem(PREFIX + key, _value)
+        fn && fn()
+    },
+    get(key, fn) {
+        if (!key) {
+            return null
+        }
+        if (typeof key === 'object') {
+            throw new Error('key不能是一个对象。')
+        }
+        var _value = sessionStorage.getItem(PREFIX + key)
+        if (_value !== null) {
+            try {
+                return JSON.parse(_value)
+            } catch (e) {}
+        }
+        return _value
+    },
+    remove(key) {
+        sessionStorage.removeItem(PREFIX + key)
+    }
+}

+ 24 - 0
src/views/dasboard/index.vue

@@ -0,0 +1,24 @@
+<template>
+    <div>
+        <h4>index</h4>
+        <button v-if="hasPermission('system:role:query')">测试权限 </button>
+    </div>
+</template>
+<script>
+export default {
+    name: 'Dasboard',
+    props: [],
+    data() {
+        return {}
+    },
+    computed: {},
+    methods: {},
+    created() {
+        // console.log('--------------------- index created')
+    },
+    mounted() {},
+    components: {}
+}
+</script>
+<style lang='scss' scoped>
+</style>

+ 155 - 0
src/views/demo/FormPageDemo.vue

@@ -0,0 +1,155 @@
+<template>
+    <div class='input-form'>
+        <el-form :model='ruleForm' :rules='rules' ref='ruleForm'>
+            <el-form-item label='活动名称' prop='name'>
+                <el-input v-model='ruleForm.name'></el-input>
+            </el-form-item>
+            <el-form-item label='活动区域' prop='region'>
+                <el-select v-model='ruleForm.region' placeholder='请选择活动区域'>
+                    <el-option label='区域一' value='shanghai'></el-option>
+                    <el-option label='区域二' value='beijing'></el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item label='活动时间' required>
+                <el-col :span='6'>
+                    <el-form-item prop='date1'>
+                        <el-date-picker type='date' placeholder='选择日期' v-model='ruleForm.date1' style='width: 100%;'></el-date-picker>
+                    </el-form-item>
+                </el-col>
+                <el-col :span='6'>
+                    <el-form-item prop='date2'>
+                        <el-time-picker
+                            type='fixed-time'
+                            placeholder='选择时间'
+                            v-model='ruleForm.date2'
+                            style='width: 100%;'
+                        ></el-time-picker>
+                    </el-form-item>
+                </el-col>
+            </el-form-item>
+
+            <el-form-item label='活动时间' required>
+                <el-col :span='6'>
+                    <el-form-item prop='date1'>
+                        <el-date-picker type='date' placeholder='选择日期' v-model='ruleForm.date1' style='width: 100%;'></el-date-picker>
+                    </el-form-item>
+                </el-col>
+                <el-col :span='6'>
+                    <el-form-item prop='date2'>
+                        <el-time-picker
+                            type='fixed-time'
+                            placeholder='选择时间'
+                            v-model='ruleForm.date2'
+                            style='width: 100%;'
+                        ></el-time-picker>
+                    </el-form-item>
+                </el-col>
+            </el-form-item>
+
+            <el-form-item label='即时配送' prop='delivery'>
+                <el-switch on-text off-text v-model='ruleForm.delivery'></el-switch>
+            </el-form-item>
+            <el-form-item label='活动性质' prop='type'>
+                <el-checkbox-group v-model='ruleForm.type'>
+                    <el-checkbox label='美食/餐厅线上活动' name='type'></el-checkbox>
+                    <el-checkbox label='地推活动' name='type'></el-checkbox>
+                    <el-checkbox label='线下主题活动' name='type'></el-checkbox>
+                    <el-checkbox label='单纯品牌曝光' name='type'></el-checkbox>
+                </el-checkbox-group>
+            </el-form-item>
+            <el-form-item label='特殊资源' prop='resource'>
+                <el-radio-group v-model='ruleForm.resource'>
+                    <el-radio label='线上品牌商赞助'></el-radio>
+                    <el-radio label='线下场地免费'></el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label='活动形式' prop='desc'>
+                <el-input type='textarea' v-model='ruleForm.desc'></el-input>
+            </el-form-item>
+            <el-form-item>
+                <el-button type='primary' @click='submitForm("ruleForm")'>立即创建</el-button>
+                <el-button @click='resetForm("ruleForm")'>重置</el-button>
+            </el-form-item>
+        </el-form>
+    </div>
+</template>
+<script>
+export default {
+    name: 'FormPageDemo',
+    props: [],
+    components: {},
+    data() {
+        return {
+            imagesArr: [
+                'http://service.sagacloud.cn:28888/image-service/common/file_get?systemId=saas&key=业主管理1537929021391.png'
+            ],
+            ruleForm: {
+                name: '',
+                region: '',
+                date1: '',
+                date2: '',
+                delivery: false,
+                type: [],
+                resource: '',
+                desc: ''
+            },
+            rules: {
+                name: [
+                    { required: true, message: '请输入活动名称', trigger: 'blur' },
+                    { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
+                ],
+                region: [{ required: true, message: '请选择活动区域', trigger: 'change' }],
+                date1: [
+                    {
+                        type: 'date',
+                        required: true,
+                        message: '请选择日期',
+                        trigger: 'change'
+                    }
+                ],
+                date2: [
+                    {
+                        type: 'date',
+                        required: true,
+                        message: '请选择时间',
+                        trigger: 'change'
+                    }
+                ],
+                type: [
+                    {
+                        type: 'array',
+                        required: true,
+                        message: '请至少选择一个活动性质',
+                        trigger: 'change'
+                    }
+                ],
+                resource: [{ required: true, message: '请选择活动资源', trigger: 'change' }],
+                desc: [{ required: true, message: '请填写活动形式', trigger: 'blur' }]
+            }
+        }
+    },
+    methods: {
+        imagesChange(keys) {
+            this.imagesArr = keys
+        },
+        submitForm(formName) {
+            this.$refs[formName].validate(valid => {
+                if (valid) {
+                    alert('submit!')
+                } else {
+                    console.log('error submit!!')
+                    return false
+                }
+            })
+        },
+        resetForm(formName) {
+            this.$refs[formName].resetFields()
+        }
+    },
+    created() {
+        this.$store.dispatch('setBreadcrumb', [{ label: 'Demo' }, { label: 'Form' }])
+    }
+}
+</script>
+<style lang='scss' scoped>
+</style>

+ 123 - 0
src/views/demo/TablePageDemo.vue

@@ -0,0 +1,123 @@
+<template>
+    <table-page-template>
+        <el-form slot='form' :inline='true' :model='formInline' size='mini'>
+            <el-form-item label='审批人'>
+                <el-input v-model='formInline.user' placeholder='审批人'></el-input>
+            </el-form-item>
+            <el-form-item label='活动区域'>
+                <el-select v-model='formInline.region' placeholder='活动区域'>
+                    <el-option label='区域一' value='shanghai'></el-option>
+                    <el-option label='区域二' value='beijing'></el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button type='primary' @click='query'>查询</el-button>
+            </el-form-item>
+            <el-form-item label='审批人'>
+                <el-input v-model='formInline.user' placeholder='审批人'></el-input>
+            </el-form-item>
+            <el-form-item label='备选项'>
+                <el-radio-group v-model='radio2'>
+                    <el-radio :label='3'>备选项3</el-radio>
+                    <el-radio :label='6'>备选项6</el-radio>
+                    <el-radio :label='9'>备选项9</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item label='审批人'>
+                <el-input v-model='formInline.user' placeholder='审批人'></el-input>
+            </el-form-item>
+            <el-form-item label='复选框'>
+                <el-checkbox-group v-model='checkList' @change='checkListChange'>
+                    <el-checkbox label='复选框 A'>复选框 1</el-checkbox>
+                    <el-checkbox label='复选框 B'></el-checkbox>
+                    <el-checkbox label='复选框 C'></el-checkbox>
+                    <el-checkbox label='禁用' disabled></el-checkbox>
+                    <el-checkbox label='选中且禁用' disabled></el-checkbox>
+                </el-checkbox-group>
+            </el-form-item>
+        </el-form>
+        <el-table
+            slot='table'
+            ref='multipleTable'
+            :data='tableData3'
+            border
+            tooltip-effect='dark'
+            style='width: 100%'
+            @selection-change='handleSelectionChange'
+        >
+            <el-table-column type='selection' width='55'></el-table-column>
+            <el-table-column label='日期' width='120'>
+                <template slot-scope='scope'>{{ scope.row.date }}</template>
+            </el-table-column>
+            <el-table-column prop='name' label='姓名' width='120'></el-table-column>
+            <el-table-column prop='address' label='地址' show-overflow-tooltip></el-table-column>
+        </el-table>
+        <base-pagination :total='456' @pageChanged='pageChanged' slot='pagination'></base-pagination>
+    </table-page-template>
+</template>
+<script>
+export default {
+    name: 'DemoTable',
+    data() {
+        return {
+            radio2: 3,
+            checkList: ['选中且禁用', '复选框 A'],
+            formInline: {
+                user: '',
+                region: ''
+            },
+            tableData3: [],
+            tableData: [
+                {
+                    date: '2016-05-03',
+                    name: '王小虎',
+                    address: '上海市普陀区金沙江路 1518 弄'
+                },
+                {
+                    date: '2016-05-02',
+                    name: '王小虎',
+                    address: '上海市普陀区金沙江路 1518 弄'
+                }
+            ],
+            multipleSelection: [],
+            total: 456
+        }
+    },
+    methods: {
+        onSubmit() {
+            console.log('submit!')
+        },
+        checkListChange() {
+            console.log('checkListChange ' + JSON.stringify(this.checkList))
+        },
+        handleSelectionChange(val) {
+            console.log('handleSelectionChange --> ' + val)
+            this.multipleSelection = val
+        },
+        handleSizeChange(size) {
+            console.log('handleSizeChange --> ' + size)
+        },
+        handleCurrentChange(page) {
+            console.log('handleCurrentChange --> ' + page)
+        },
+        query() {
+            this.postJson('/server/getTableData', {}, resp => {
+                console.log(resp)
+            })
+        },
+        pageChanged(page, size) {
+            console.log('pageChanged size=' + size + ', page = ' + page)
+        }
+    },
+    created() {
+        this.$store.dispatch('setBreadcrumb', [{ label: 'Demo' }, { label: 'Table' }])
+        for (let i = 0; i < 50; i++) {
+            this.tableData3.push({
+                date: '2016-05-03',
+                name: '王小虎',
+                address: '上海市普陀区金沙江路 1518 弄'
+            })
+        }
+    }
+}
+</script>

+ 40 - 0
src/views/system/auth/index.vue

@@ -0,0 +1,40 @@
+<template>
+    <div>
+        <h4>请稍候...</h4>
+    </div>
+</template>
+<script>
+import { mapGetters, mapMutations } from 'vuex'
+import adminapi from '@/api/system/admin'
+import authutils from '@/utils/authutils'
+export default {
+    name: 'index',
+    props: [],
+    data() {
+        return {}
+    },
+    created() {
+        // console.log('--------------------- index created')
+        // console.log(this.$route.query)
+        let token = this.$route.query.token
+        this.$store.commit('setSsoToken', token)   
+             
+        this.$store.dispatch('layout/loadUserInfo').then(resp => {
+            console.log('store dispatch result ', resp)
+            if (resp.result == 'success') {
+                let lastRoute = this.$store.getters['lastRoute']
+                console.log('last route ', lastRoute)
+                if (lastRoute) {
+                    this.$router.replace(lastRoute)
+                } else {
+                    this.$router.replace('/')
+                }
+            } else {
+                authutils.toLoginPage()
+            }
+        })
+    },
+    mounted() {},
+    components: {}
+}
+</script>

+ 103 - 0
src/views/system/pwd/ChangePwd.vue

@@ -0,0 +1,103 @@
+<template>
+    <div>
+        <h2>修改密码</h2>
+        <el-form :model='ruleForm' :rules='rules' ref='ruleForm' label-width='150px' class='demo-ruleForm'>
+            <el-form-item label='原密码' prop='oldPwd'>
+                <el-input
+                    name='password'
+                    type='password'
+                    v-model='ruleForm.oldPwd'
+                    placeholder='原密码'
+                    style='width: 200px'
+                />
+            </el-form-item>
+            <el-form-item label='新密码' prop='newPwd'>
+                <el-input
+                    name='password'
+                    type='password'
+                    v-model='ruleForm.newPwd'
+                    placeholder='新密码'
+                    style='width: 200px'
+                />
+            </el-form-item>
+            <el-form-item label='再次输入新密码' prop='newPwd2'>
+                <el-input
+                    name='password'
+                    type='password'
+                    v-model='ruleForm.newPwd2'
+                    placeholder='再次输入新密码'
+                    style='width: 200px'
+                />
+            </el-form-item>
+            <el-form-item>
+                <el-button type='primary' @click='submitForm("ruleForm")'>确定</el-button>
+            </el-form-item>
+        </el-form>
+    </div>
+</template>
+<script>
+export default {
+    name: 'ChangePwd',
+
+    data() {
+        let checkPwd2 = function(rule, value, callback) {
+            if (result.ruleForm.newPwd === result.ruleForm.newPwd2) {
+                callback()
+            } else {
+                callback(new Error('两次输入的密码不一致!'))
+            }
+        }
+        let result = {
+            ruleForm: {
+                oldPwd: '',
+                newPwd: '',
+                newPwd2: ''
+            },
+            rules: {
+                oldPwd: [
+                    { required: true, message: '请输入原密码', trigger: 'blur' },
+                    { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
+                ],
+                newPwd: [
+                    { required: true, message: '请输入新密码', trigger: 'blur' },
+                    { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
+                ],
+                newPwd2: [
+                    { required: true, message: '请重复输入新密码', trigger: 'blur' },
+                    { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
+                    { validator: checkPwd2, trigger: 'blur' }
+                ]
+            }
+        }
+        return result
+    },
+
+    methods: {
+        submitForm(formName) {
+            this.$refs['ruleForm'].validate(valid => {
+                if (valid) {
+                    let vm = this
+                    // this.postJson(
+                    //     '/admin/user/chgpwd',
+                    //     { oldPwd: this.ruleForm.oldPwd, newPwd: this.ruleForm.newPwd },
+                    //     resp => {
+                    //         vm.$message({
+                    //             type: 'success',
+                    //             message: '修改密码成功'
+                    //         })
+                    //     }
+                    // )
+                }
+            })
+        },
+        resetForm(formName) {
+            this.$refs[formName].resetFields()
+        }
+    },
+    created() {
+        this.$store.dispatch('setBreadcrumb', [{ label: '修改密码' }])
+    }
+}
+</script>
+<style lang="css" scoped>
+</style>

+ 0 - 0
static/.gitkeep