Browse Source

'初始化'

zhangyu 4 years ago
commit
f17c267916
79 changed files with 9475 additions and 0 deletions
  1. 12 0
      .babelrc
  2. 9 0
      .editorconfig
  3. 15 0
      .gitignore
  4. 10 0
      .postcssrc.js
  5. 160 0
      README.md
  6. 45 0
      build/build.js
  7. 56 0
      build/check-versions.js
  8. BIN
      build/logo.png
  9. 99 0
      build/utils.js
  10. 20 0
      build/vue-loader.conf.js
  11. 99 0
      build/webpack.base.conf.js
  12. 93 0
      build/webpack.dev.conf.js
  13. 140 0
      build/webpack.prod.conf.js
  14. 15 0
      config/dev.env.js
  15. 153 0
      config/index.js
  16. 12 0
      config/prod.env.js
  17. 15 0
      index.html
  18. 82 0
      package.json
  19. 40 0
      publish.js
  20. 13 0
      src/App.vue
  21. 110 0
      src/api/framework.js
  22. 100 0
      src/api/httputils.js
  23. 21 0
      src/api/scan/config.js
  24. 41 0
      src/api/scan/fetch.js
  25. 54 0
      src/api/scan/httpUtil.js
  26. 2049 0
      src/api/scan/request.js
  27. 30 0
      src/api/uploader/index.js
  28. BIN
      src/assets/css/chosen-sprite.png
  29. BIN
      src/assets/css/chosen-sprite@2x.png
  30. 492 0
      src/assets/css/chosen.css
  31. 477 0
      src/assets/css/jsmind.css
  32. 2 0
      src/assets/image/uncultivated.svg
  33. BIN
      src/assets/logo.png
  34. 489 0
      src/assets/style/iconfont/iconfont.css
  35. BIN
      src/assets/style/iconfont/iconfont.eot
  36. 380 0
      src/assets/style/iconfont/iconfont.svg
  37. BIN
      src/assets/style/iconfont/iconfont.ttf
  38. BIN
      src/assets/style/iconfont/iconfont.woff
  39. 100 0
      src/assets/style/style.scss
  40. 84 0
      src/components/dasboard/index.vue
  41. 326 0
      src/data/menus.js
  42. 428 0
      src/framework/components/messagesever/index.vue
  43. 9 0
      src/framework/components/messagesever/mqSetting.js
  44. 43 0
      src/framework/components/messagesever/msmq.js
  45. 182 0
      src/framework/layout/Login.vue
  46. 87 0
      src/framework/layout/Main.vue
  47. 139 0
      src/framework/layout/PageHeader.vue
  48. 91 0
      src/framework/layout/PageSidebar.vue
  49. 153 0
      src/framework/layout/layout-store.js
  50. 32 0
      src/framework/plugins/components.js
  51. 424 0
      src/framework/style/layout.scss
  52. 106 0
      src/framework/style/reset.scss
  53. 106 0
      src/framework/style/theme.scss
  54. 32 0
      src/framework/template/TablePageTemplate.vue
  55. 88 0
      src/framework/utils/basicutils.js
  56. 33 0
      src/framework/utils/storage.js
  57. 32 0
      src/main.js
  58. 18 0
      src/router/index.js
  59. 17 0
      src/router/system.js
  60. 38 0
      src/store/index.js
  61. 91 0
      src/utils/authutils.js
  62. 2 0
      src/utils/bus.js
  63. 381 0
      src/utils/getFirstLetter.js
  64. 107 0
      src/utils/httputils.js
  65. 33 0
      src/utils/localStorage.js
  66. 64 0
      src/utils/tools.js
  67. 36 0
      src/views/index/index.vue
  68. 39 0
      src/views/system/auth/index.vue
  69. 23 0
      src/views/system/nouser/index.vue
  70. 103 0
      src/views/system/pwd/ChangePwd.vue
  71. 78 0
      src/views/system/role/RoleEdit.vue
  72. 92 0
      src/views/system/role/RoleList.vue
  73. 188 0
      src/views/system/role/RoleManage.vue
  74. 132 0
      src/views/system/role/eleTreeUtils.js
  75. 100 0
      src/views/system/user/UserAdd.vue
  76. 87 0
      src/views/system/user/UserList.vue
  77. 0 0
      static/.gitkeep
  78. BIN
      static/favicon.ico
  79. 18 0
      tsconfig.json

+ 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 = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+.DS_Store
+node_modules/
+/dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+package-lock.json*
+
+# 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": {}
+  }
+}

+ 160 - 0
README.md

@@ -0,0 +1,160 @@
+# sagacloud-admin
+
+> A Vue.js project
+
+## Build Setup
+
+```bash
+npm install
+npm install vuex axios element-ui echarts font-awesome
+npm install node-sass sass-loader --save-dev
+
+# serve with hot reload at localhost:8080
+npm run dev
+# build for production with minification
+npm run build
+# build for production and view the bundle analyzer report
+npm run build --report
+```
+
+## 项目结构说明
+
+#### 目录结构
+
+```
+│  .editorconfig                        # 格式化配置,修改缩进为2
+│  index.html                           # 修改页面title
+│  package.json                         # 包
+│  README.md                            # 说明文档
+│
+├─build                                 # 不变
+│
+├─config
+│      dev.env.js                       # 开发环境变量
+│      index.js                         # 设置代理
+│      prod.env.js                      # 生产环境变量
+│
+├─src
+│  ├─api                                # 业务逻辑api, http的请求都放在这里
+│  │  |─framework.js                    # 框架预留接口,实现登录,权限校验等功能
+│  │  └─httputils.js                    # http 工具类
+│  │  └─msgsever.js                     # 消息中心接口
+│  ├─assets                             # 静态资源, 替换logo图片
+│  ├─components                         # 业务相关组件
+│  ├─data                               # 业务相关静态数据
+│  │      menus.js                      # 菜单数据
+│  ├─framework                          # 框架主目录
+│  │  ├─components                      # 业务无关组件
+│  │  │
+│  │  ├─demo                            # Demo主目录
+│  │  │  │  demo-menus.js               # Demo按钮
+│  │  │  │  demo-routes.js              # Demo路由
+│  │  │  └─views                        # Demo页面
+│  │  │
+│  │  ├─layout                          # 框架主目录
+│  │  │      layout-store.js            # 框架状态定义
+│  │  │      Login.vue                  # 登录页
+│  │  │      Main.vue                   # 框架主页
+│  │  │      PageHeader.vue             # 顶部栏
+│  │  │      PageSidebar.vue            # 左侧菜单栏
+│  │  │
+│  │  ├─plugins                         # 插件,业务无关
+│  │  │      components.js
+│  │  ├─style                           # 样式
+│  │  │
+│  │  ├─template                        # 页面模板
+│  │  │      TablePageTemplate.vue
+│  │  │
+│  │  └─utils                           # 工具类,业务无关
+│  │
+│  ├─router                             # 主路由
+│  │      index.js
+│  │      system.js
+│  │
+│  ├─store                              # vuex
+│  │      index.js
+│  │
+│  └─views                              # 业务页面主路径
+|
+└─static
+        .gitkeep
+```
+
+#### /api/framework.js
+
+必须实现以下方法
+
+```
+export default {
+
+    // 路由拦截
+    routerBeforeEach: async function(to, from, next){},
+
+    // 获取左侧菜单数据
+    getMenus: function(permissions){},
+
+    // 跳转到登录页面
+    toLoginPage: function(){},
+
+    // 登录接口
+    login: function(username, password){}
+
+    // 加载当前登录用户信息
+    loadUserInfo: function(){},
+
+    // 退出登录
+    toLogout: function(){}
+}
+
+```
+
+#### 工具类说明
+
+/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的所有元素删除
+```
+
+/src/api/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)                        // 在浏览器缓存中删除数据
+```
+
+#### 设置面包屑方法
+
+```
+在路由router中配置meta.breadcrumbs,在进入路由页面时会自动解析为面包屑
+this.$store.dispatch('setBreadcrumb', [{ label: 'Demo' }, { label: 'Form'  ])
+this.$store.dispatch('setBreadcrumb', [{ label: 'Demo', path: '' }, { label: 'Form' , path: ''}])
+```
+#### 上传管理器调用方法(上传较大文件时使用)
+
+```
+在组件中引入Bus中央时间总线
+    Bus.$emit('openUploader', query, file)    //添加文件到上传管理器中, query为参数, file为H5原生的文件对象
+    Bus.$on('fileAdded', () => {})            //文件选择后的回调
+    Bus.$on('fileSuccess', () => {})          //文件上传成功后的回调
+```
+
+## 接口文档说明
+
+- [数据字典接口文档](http://39.106.8.246:3003/sagacloud/paas-doc/src/master/%E6%95%B0%E6%8D%AE%E5%AD%97%E5%85%B8%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3.md)
+
+- [物理世界接口文档](http://39.106.8.246:3003/sagacloud/paas-doc/src/master/%E7%89%A9%E7%90%86%E4%B8%96%E7%95%8C%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3.md)
+
+- [通用查询](http://39.106.8.246:3003/sagacloud/paas-doc/src/master/%E9%80%9A%E7%94%A8%E6%9F%A5%E8%AF%A2.md)

+ 45 - 0
build/build.js

@@ -0,0 +1,45 @@
+'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"
+            )
+        )
+    })
+})

+ 56 - 0
build/check-versions.js

@@ -0,0 +1,56 @@
+'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)
+    }
+}

BIN
build/logo.png


+ 99 - 0
build/utils.js

@@ -0,0 +1,99 @@
+'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(),
+        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')
+        })
+    }
+}

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

@@ -0,0 +1,20 @@
+'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'
+    }
+}

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

@@ -0,0 +1,99 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const config = require('../config')
+const vueLoaderConfig = require('./vue-loader.conf')
+const webpack = require('webpack')
+
+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: ['.ts', '.js', '.vue', '.json'],
+        alias: {
+            'vue$': 'vue/dist/vue.esm.js',
+            '@': resolve('src'),
+            'jquery': 'jquery',
+        }
+    },
+    // 增加一个plugins
+    plugins: [
+        new webpack.ProvidePlugin({
+            $: "jquery",
+            jQuery: "jquery"
+        })
+    ],
+    module: {
+        rules: [
+            {
+                test: /\.vue$/,
+                loader: 'vue-loader',
+                options: vueLoaderConfig
+            },
+            {
+                test: /\.tsx?$/,
+                loader: 'ts-loader',
+                exclude: /node_modules/,
+                options: {
+                  appendTsSuffixTo: [/\.vue$/],
+                }
+            },
+            {
+                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'
+    }
+}

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

@@ -0,0 +1,93 @@
+'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,
+            favicon: path.resolve(__dirname, '../static/favicon.ico')
+        }),
+        // 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)
+        }
+    })
+})

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

@@ -0,0 +1,140 @@
+'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',
+            favicon: path.resolve(__dirname, '../static/favicon.ico')
+        }),
+        // 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

+ 15 - 0
config/dev.env.js

@@ -0,0 +1,15 @@
+'use strict'
+const merge = require('webpack-merge')
+const prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+    NODE_ENV: '"development"',
+    // BASE_URL: '"http://172.16.44.215"', //测试iframe地址
+    // SSO_SERVER: '"http://172.16.44.235:8081"', //测试环境
+    // BASE_URL: '"http://192.168.20.236"', //(新)测试iframe地址
+    // SSO_SERVER: '"http://192.168.20.236:8086"', //(新)测试环境
+    // MQTT_SERVICE: '"ws://172.16.42.210:61614/stomp/"' //MQ测试环境地址
+    SSO_SERVER: '"http://sso.sagacloud.cn"',  //正式环境
+    BASE_URL: '"http://mbi.sagacloud.cn"', //线上iframe地址
+    MQTT_SERVICE: '"ws://adm.sagacloud.cn/stomp/"' //MQ正式环境地址
+})

+ 153 - 0
config/index.js

@@ -0,0 +1,153 @@
+'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://mbi.sagacloud.cn:8080',
+        changeOrigin: true,
+        pathRewrite: {
+          "^/admin": "/"
+        }
+      },
+      '/api': {
+        target: 'http://mbi.sagacloud.cn:8080',
+        changeOrigin: true,
+        pathRewrite: {
+          "^/api": "/"
+        }
+      },
+      '/data-platform-3': {
+        // 目标 API 地址
+        target: 'http://api.sagacloud.cn/',
+        // 如果要代理 websockets
+        ws: true,
+        // 将主机标头的原点更改为目标URL
+        changeOrigin: false
+      },
+      '/business-space': {
+        // 目标 API 地址
+        target: 'http://api.sagacloud.cn',
+        changeOrigin: true,
+        pathRewrite: {
+          "^/business-space": "/dp-auxiliary/business-space/"
+        }
+      },
+      '/pointconfig': {
+        // 目标 API 地址
+        target: 'http://mbi.sagacloud.cn:8080/',
+        // 如果要代理 websockets
+        ws: true,
+        // 将主机标头的原点更改为目标URL
+        changeOrigin: false
+      },
+      '/venders-dp': {
+        // 目标 API 地址
+        target: 'http://api.sagacloud.cn',
+        changeOrigin: true,
+        pathRewrite: {
+          "^/venders-dp": "/dp-auxiliary/venders-dp/"
+        }
+      },
+      '/venders': {
+        // 目标 API 地址
+        target: 'http://api.sagacloud.cn',
+        changeOrigin: true,
+        pathRewrite: {
+          "^/venders": "/dp-auxiliary/venders/"
+        }
+      },
+      '/ScanBuilding': {
+        // 目标 API 地址
+        target: 'http://mbi.sagacloud.cn:8080/',
+        // 如果要代理 websockets
+        ws: true,
+        // 将主机标头的原点更改为目标URL
+        changeOrigin: false
+      },
+      '/image-service': {
+        // 目标 API 地址
+        target: 'http://api.sagacloud.cn',
+        changeOrigin: true,
+        pathRewrite: {
+          "^/image-service": "/dp-auxiliary/image-service/"
+        }
+      },
+      '/modelapi': {
+        target: 'http://mbi.sagacloud.cn:8080',
+        changeOrigin: true,
+        pathRewrite: {
+          "^/modelapi": "/revit-algorithm/"
+        }
+      },
+      '/schedulerapi': {
+        target: 'http://mbi.sagacloud.cn:8080',
+        changeOrigin: true,
+        pathRewrite: {
+            "^/schedulerapi": "/scheduler/"
+        }
+      },
+    },
+
+    // Various Dev Server settings
+    host: '0.0.0.0', // 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
+  }
+}

+ 12 - 0
config/prod.env.js

@@ -0,0 +1,12 @@
+'use strict'
+module.exports = {
+    NODE_ENV: '"production"',
+    BASE_URL: '"http://172.16.44.215"', //测试iframe地址
+    SSO_SERVER: '"http://172.16.44.235:8081"', //测试环境
+    // BASE_URL: '"http://192.168.20.236"', //(新)测试iframe地址
+    // SSO_SERVER: '"http://192.168.20.236:8086"', //(新)测试环境
+    MQTT_SERVICE: '"ws://172.16.42.210:61614/stomp/"' //MQ测试环境地址
+    // BASE_URL: '"http://mbi.sagacloud.cn"', //线上iframe地址
+    // SSO_SERVER: '"http://sso.sagacloud.cn"',  //正式环境
+    // MQTT_SERVICE: '"ws://adm.sagacloud.cn/stomp/"' //MQ正式环境地址
+}

+ 15 - 0
index.html

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <link rel="stylesheet" type="text/css" href="//at.alicdn.com/t/font_1318214_i7toym6dora.css">
+    <title>sagacloud-admin</title>
+</head>
+
+<body>
+    <div id="app"></div>
+</body>
+
+</html>

+ 82 - 0
package.json

@@ -0,0 +1,82 @@
+{
+  "name": "sagacloud-midrange",
+  "version": "1.0.0",
+  "description": "A Vue.js project",
+  "author": "zy <zhangyu@sagacloud.cn>",
+  "private": true,
+  "remote": {
+    "host": "172.16.39.206",
+    "path": "/home/pages/sagacloud-midrange/dist",
+    "user": "root",
+    "password": "saga",
+    "local": "dist"
+  },
+  "scripts": {
+    "i": "npm install",
+    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
+    "start": "npm run dev",
+    "build": "node build/build.js",
+    "publish": "node publish.js"
+  },
+  "dependencies": {
+    "axios": "^0.18.0",
+    "element-ui": "^2.13.2",
+    "vue": "^2.5.2",
+    "vue-axios": "^2.1.4",
+    "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",
+    "babel-runtime": "^6.26.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",
+    "file-saver": "^2.0.2",
+    "friendly-errors-webpack-plugin": "^1.6.1",
+    "html-webpack-plugin": "^2.30.1",
+    "node-notifier": "^5.1.2",
+    "node-sass": "^4.11.0",
+    "node-ssh": "^6.0.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",
+    "stompjs": "^2.3.3",
+    "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"
+  ]
+}

+ 40 - 0
publish.js

@@ -0,0 +1,40 @@
+/*
+ * @Author: zhangyu
+ * @Date: 2019-12-18 16:18:30
+ * @Info: 自动化部署
+ * @LastEditTime : 2020-02-11 17:47:56
+ */
+
+const Client = require("node-ssh");
+const ssh = new Client(); 
+
+ssh.connect({
+  host: process.env.npm_package_remote_host,
+  port: "22",
+  // privateKey: `${process.env.HOME}\\.ssh\\id_rsa_2048`,
+  username: process.env.npm_package_remote_user,
+  password: process.env.npm_package_remote_password
+}).then(() => {
+  const failedList = [];
+  ssh.putDirectory(
+    process.env.npm_package_remote_local,
+    process.env.npm_package_remote_path,
+    {
+      recursive: true,
+      concurrency: 1,
+      tick: function(localPath, remotePath, error) {
+        if (error) {
+          failedList.push(localPath);
+        }
+      }
+    }
+  ).then(status => {
+    if (failedList.length > 0) {
+      console.log("发布失败");
+      console.log("failed transfers", failedList.join(", "));
+    } else {
+      console.log(status ? "发布成功" : "发布失败");
+    }
+    ssh.dispose();
+  });
+});

+ 13 - 0
src/App.vue

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

+ 110 - 0
src/api/framework.js

@@ -0,0 +1,110 @@
+import store from '@/store'
+import menus from '@/data/menus'
+import router from '@/router'
+import httputils from '@/utils/httputils'
+// const userInfo = { username: 'admin', permissions: ['demo'] }
+
+const userInfo = null
+function toLogin() {
+    router.push('/login')
+}
+
+function checkMenu(menu, ps) {
+    let result = { name: menu.name, icon: menu.icon, path: menu.path, disabled: menu.disabled }
+    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' || to.path == '/login') {
+            next()
+        } else {
+            if (to.meta.breadcrumbs) {
+                store.dispatch('setBreadcrumb', to.meta.breadcrumbs)
+            } else {
+                store.dispatch('setBreadcrumb', [])
+            }
+            // next()
+            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 {
+                if (to.meta.breadcrumbs) {
+                    store.dispatch('setBreadcrumb', to.meta.breadcrumbs)
+                }
+                next()
+            }
+            return true
+        }
+    },
+
+    getMenus(permissions) {
+        let result = []
+        let allMenus = []
+        // 开发环境下展示demo页面
+        // if (process.env.NODE_ENV === 'development') {
+        //     allMenus = demoMenus
+        // }
+        allMenus = allMenus.concat(menus)
+        let ps = !permissions ? {} : permissions
+        console.log(ps)
+        allMenus.forEach(item => {
+            let menu = checkMenu(item, ps)
+            if (menu) {
+                result.push(menu)
+            }
+        })
+        return result
+    },
+
+    login(username, password) {
+        return new Promise((resolve, reject) => {
+            if (username == 'admin') {
+                resolve({ result: 'success', ssoToken: 'admin-token' })
+            } else {
+                resolve({ result: 'fail', message: 'username or password error' })
+            }
+        })
+    },
+
+    loadUserInfo() {
+        // return new Promise((resolve, reject) => resolve({ result: 'success', ...userInfo }))
+        return httputils.getJson(`/admin/ibms/user/userInfo`)
+    },
+
+    toLogout() {
+        // TODO
+        router.push('/auth')
+        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
+}

+ 100 - 0
src/api/httputils.js

@@ -0,0 +1,100 @@
+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: 3000000, // 请求超时时间
+    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) {
+        //在这里对返回的数据进行处理
+        let resp = res.data
+        if (resp.result === 'unauthc') {
+            store.commit('logined', false)
+            MessageBox.confirm('未登陆或登陆信息已失效, 是否重新登陆?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'error'
+            })
+                .then(resp => {
+                    window.location.reload()
+                })
+                .catch(error => {
+                    console.log('')
+                })
+        }
+        return res
+    },
+    function(err) {
+        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
+}

+ 21 - 0
src/api/scan/config.js

@@ -0,0 +1,21 @@
+export const baseUrl = '/api';
+export const api = '/ScanBuilding';
+export const sass = 'sass';
+export const physics = '/data-platform-3';
+export const business = '/business-space'
+export const venders = '/venders'
+export const zone = {
+  GeneralZone: 'zone-general', //默认分区
+  PowerSupplyZone: 'zone-power-supply', //供电分区
+  LightingZone: 'zone-lighting', //照明分区
+  AirConditioningZone: 'metaspace', //空调分区
+  TenantZone: 'zone-tenant', //租赁分区
+  HeatingZone:'zone-heating', //采暖分区
+  CleanZone:'zone-clean', //洁净分区
+  DomesticWaterSupplyZone:'zone-domestic-water', //生活给水分区
+  NetworkZone:'zone-network', //网络分区
+  FunctionZone:'zone-function', //功能分区
+  FireZone:'zone-fire', //防火分区
+  SecurityZone:'zone-security', //安防分区
+  Ispace:'ispace', //元空间
+}

+ 41 - 0
src/api/scan/fetch.js

@@ -0,0 +1,41 @@
+import Vue from 'vue'
+import axios from 'axios'
+import vueAxios from 'vue-axios'
+import { Message } from 'element-ui';
+
+Vue.use(vueAxios, axios)
+//创建实例
+
+const service = axios.create({
+  timeout: 3000000, //请求时间超出
+  withCredentials: true, //是否跨站点访问控制请求
+})
+
+//request拦截
+service.interceptors.request.use(config => {
+  return config
+}, error => {
+  console.log(error)
+  Promise.reject(error)
+})
+
+//response拦截器
+service.interceptors.response.use(
+  response => {
+    // let resp = response.data;
+    // let result = resp.result || resp.Result
+    return response
+    // if (result == 'success' || result == 'Success') {
+    //     return response
+    // } else {
+    //     let msg = resp.message ? resp.message : resp.ResultMsg? resp.ResultMsg: resp.Message;
+    //     Message.error({ message: msg });
+    // }
+  },
+  error => {
+    console.log('err' + error)
+    return Promise.reject(error)
+  }
+)
+
+export default service

+ 54 - 0
src/api/scan/httpUtil.js

@@ -0,0 +1,54 @@
+import {Message} from 'element-ui';
+import fetch from './fetch'
+import storage from '@/framework/utils/storage'
+
+function successResponse(vm, response, success, failed) {
+  let resp = response.data;
+  let result = resp.result ? resp.result : resp.Result;
+  if (result === 'success') {
+    if (success) {
+      success(resp);
+    }
+  } else {
+    let msg = resp.message ? resp.message : resp.ResultMsg || resp.resultMsg || resp.Message;
+    Message.error({ message: msg });
+    if (failed) {
+      failed(resp);
+    }
+  }
+
+}
+
+function errorResponse(vm, response, err) {
+  let json = JSON.parse(JSON.stringify(response))
+  console.error(response)
+  if (json.response) {
+    Message.error({ message: `接口:${json.response.config.url}请求错误,错误状态为:${json.response.status}` })
+  } else {
+    console.error(vm, response, err)
+    Message.error({ message: '请求错误' });
+  }
+}
+
+export default {
+  getJson: function(url, data, success, failed, err) {
+    let ProjectId = localStorage.getItem("projectId")
+    let userName = storage.get("user_name")
+    let vm = this;
+    fetch({ url: url, method: 'get', params: data, headers: {'ProjectId': ProjectId, 'Comming': 'adm' ,'Account': userName}}).then((response) => {
+      successResponse(vm, response, success, failed)
+    }).catch(error => {
+      errorResponse(vm, error, err);
+    });
+  },
+  postJson: function(url, data, success, failed, err) {
+    let ProjectId = localStorage.getItem("projectId")
+    let userName = storage.get("user_name")
+    let vm = this;
+    fetch({ url: url, method: 'post', data: data, headers: {'ProjectId': ProjectId, 'Comming': 'adm' ,'Account': userName} }).then((response) => {
+      successResponse(vm, response, success, failed)
+    }).catch(error => {
+      errorResponse(vm, error, err);
+    });
+  }
+}

File diff suppressed because it is too large
+ 2049 - 0
src/api/scan/request.js


+ 30 - 0
src/api/uploader/index.js

@@ -0,0 +1,30 @@
+import http from '../scan/httpUtil'
+
+const baseUrl = '/image-service'
+
+//分片上传文件接口
+
+/**
+ * info: 申请分片上传接口(获取上传UploadId)
+ * @param {systemId:string} param 系统的名称
+ * @param {secret:string} param 系统的密码
+ * @param {key:string} param  成功的回上传的文件名(可以包含文件路径)
+ * @param {overwrite:boolean} param 表示是否覆盖已有的文件(如果存在) 注:overwrite有一定风险, 只在注册分片时校验, 合并时候如果key被占用, 会覆盖掉之前文件
+ * 
+ */
+export function getUploadId(param, success) {
+  let url = ` ${baseUrl}/common/register_multipart_upload?systemId=${param.systemId}&secret=${param.secret}&key=${param.key}&overwrite=${param.overwrite}`
+  http.postJson(url, {}, success)
+}
+
+/**
+ * info: 合并分片的接口
+ * @param {systemId:string} param 系统的名称
+ * @param {secret:string} param 系统的密码
+ * @param {uploadId:string} param  需要合并的上传文件的uploadId
+ * 
+ */
+export function mergeMultipart(param, success) {
+  let url = ` ${baseUrl}/common/merge_multipart?systemId=${param.systemId}&secret=${param.secret}&uploadId=${param.uploadId}`
+  http.postJson(url, {}, success)
+}

BIN
src/assets/css/chosen-sprite.png


BIN
src/assets/css/chosen-sprite@2x.png


+ 492 - 0
src/assets/css/chosen.css

@@ -0,0 +1,492 @@
+/*!
+Chosen, a Select Box Enhancer for jQuery and Prototype
+by Patrick Filler for Harvest, http://getharvest.com
+
+Version 1.8.5
+Full source at https://github.com/harvesthq/chosen
+Copyright (c) 2011-2018 Harvest http://getharvest.com
+
+MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
+This file is generated by `grunt build`, do not edit it by hand.
+*/
+
+.chosen-container.chosen-with-drop-top .chosen-drop {
+    left: 0;
+    top: -220px;
+}
+
+.chosen-container {
+    position: relative;
+    display: inline-block;
+    vertical-align: middle;
+    font-size: 13px;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none
+}
+
+.chosen-container * {
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box
+}
+
+.chosen-container .chosen-drop {
+    position: absolute;
+    top: 100%;
+    z-index: 1010;
+    width: 100%;
+    border: 1px solid #aaa;
+    border-top: 0;
+    background: #fff;
+    -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+    box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+    display: none;
+    box-sizing: border-box;
+}
+
+.chosen-container.chosen-with-drop .chosen-drop {
+    display: block
+}
+
+.chosen-container a {
+    cursor: pointer
+}
+
+.chosen-container .chosen-single .group-name,
+.chosen-container .search-choice .group-name {
+    margin-right: 4px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    font-weight: 400;
+    color: #999
+}
+
+.chosen-container .chosen-single .group-name:after,
+.chosen-container .search-choice .group-name:after {
+    content: ":";
+    padding-left: 2px;
+    vertical-align: top
+}
+
+.chosen-container-single .chosen-single {
+    position: relative;
+    display: block;
+    overflow: hidden;
+    padding: 0 0 0 8px;
+    height: 25px;
+    border: 1px solid #aaa;
+    border-radius: 5px;
+    background-color: #fff;
+    background: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #fff), color-stop(50%, #f6f6f6), color-stop(52%, #eee), to(#f4f4f4));
+    background: linear-gradient(#fff 20%, #f6f6f6 50%, #eee 52%, #f4f4f4 100%);
+    background-clip: padding-box;
+    -webkit-box-shadow: 0 0 3px #fff inset, 0 1px 1px rgba(0, 0, 0, .1);
+    box-shadow: 0 0 3px #fff inset, 0 1px 1px rgba(0, 0, 0, .1);
+    color: #444;
+    text-decoration: none;
+    white-space: nowrap;
+    line-height: 24px
+}
+
+.chosen-container-single .chosen-single input[type=text] {
+    cursor: pointer;
+    opacity: 0;
+    position: absolute;
+    width: 0
+}
+
+.chosen-container-single .chosen-default {
+    color: #999
+}
+
+.chosen-container-single .chosen-single span {
+    display: block;
+    overflow: hidden;
+    margin-right: 26px;
+    text-overflow: ellipsis;
+    white-space: nowrap
+}
+
+.chosen-container-single .chosen-single-with-deselect span {
+    margin-right: 38px
+}
+
+.chosen-container-single .chosen-single abbr {
+    position: absolute;
+    top: 6px;
+    right: 26px;
+    display: block;
+    width: 12px;
+    height: 12px;
+    background: url(chosen-sprite.png) -42px 1px no-repeat;
+    font-size: 1px
+}
+
+.chosen-container-single .chosen-single abbr:hover {
+    background-position: -42px -10px
+}
+
+.chosen-container-single.chosen-disabled .chosen-single abbr:hover {
+    background-position: -42px -10px
+}
+
+.chosen-container-single .chosen-single div {
+    position: absolute;
+    top: 0;
+    right: 0;
+    display: block;
+    width: 18px;
+    height: 100%
+}
+
+.chosen-container-single .chosen-single div b {
+    display: block;
+    width: 100%;
+    height: 100%;
+    background: url(chosen-sprite.png) no-repeat 0 2px
+}
+
+.chosen-container-single .chosen-search {
+    position: relative;
+    z-index: 1010;
+    margin: 0;
+    padding: 3px 4px;
+    white-space: nowrap
+}
+
+.chosen-container-single .chosen-search input[type=text] {
+    margin: 1px 0;
+    padding: 4px 20px 4px 5px;
+    width: 100%;
+    height: auto;
+    outline: 0;
+    border: 1px solid #aaa;
+    background: url(chosen-sprite.png) no-repeat 100% -20px;
+    font-size: 1em;
+    font-family: sans-serif;
+    line-height: normal;
+    border-radius: 0;
+    box-sizing: border-box;
+}
+
+.chosen-container-single .chosen-drop {
+    margin-top: -1px;
+    border-radius: 0 0 4px 4px;
+    background-clip: padding-box
+}
+
+.chosen-container-single.chosen-container-single-nosearch .chosen-search {
+    position: absolute;
+    opacity: 0;
+    pointer-events: none
+}
+
+.chosen-container .chosen-results {
+    color: #444;
+    position: relative;
+    overflow-x: hidden;
+    overflow-y: auto;
+    margin: 0 4px 4px 0;
+    padding: 0 0 0 4px;
+    max-height: 100px;
+    -webkit-overflow-scrolling: touch
+}
+
+.chosen-container .chosen-results li {
+    display: none;
+    margin: 0;
+    padding: 5px 6px;
+    list-style: none;
+    line-height: 15px;
+    word-wrap: break-word;
+    -webkit-touch-callout: none
+}
+
+.chosen-container .chosen-results li.active-result {
+    display: list-item;
+    cursor: pointer
+}
+
+.chosen-container .chosen-results li.disabled-result {
+    display: list-item;
+    color: #ccc;
+    cursor: default
+}
+
+.chosen-container .chosen-results li.highlighted {
+    background-color: #3875d7;
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #3875d7), color-stop(90%, #2a62bc));
+    background-image: linear-gradient(#3875d7 20%, #2a62bc 90%);
+    color: #fff
+}
+
+.chosen-container .chosen-results li.no-results {
+    color: #777;
+    display: list-item;
+    background: #f4f4f4
+}
+
+.chosen-container .chosen-results li.group-result {
+    display: list-item;
+    font-weight: 700;
+    cursor: default
+}
+
+.chosen-container .chosen-results li.group-option {
+    padding-left: 15px
+}
+
+.chosen-container .chosen-results li em {
+    font-style: normal;
+    text-decoration: underline
+}
+
+.chosen-container-multi .chosen-choices {
+    position: relative;
+    overflow: hidden;
+    margin: 0;
+    padding: 0 5px;
+    width: 100%;
+    height: auto;
+    border: 1px solid #aaa;
+    background-color: #fff;
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(1%, #eee), color-stop(15%, #fff));
+    background-image: linear-gradient(#eee 1%, #fff 15%);
+    cursor: text
+}
+
+.chosen-container-multi .chosen-choices li {
+    float: left;
+    list-style: none
+}
+
+.chosen-container-multi .chosen-choices li.search-field {
+    margin: 0;
+    padding: 0;
+    white-space: nowrap
+}
+
+.chosen-container-multi .chosen-choices li.search-field input[type=text] {
+    margin: 1px 0;
+    padding: 0;
+    height: 25px;
+    outline: 0;
+    border: 0!important;
+    background: 0 0!important;
+    -webkit-box-shadow: none;
+    box-shadow: none;
+    color: #999;
+    font-size: 100%;
+    font-family: sans-serif;
+    line-height: normal;
+    border-radius: 0;
+    width: 25px
+}
+
+.chosen-container-multi .chosen-choices li.search-choice {
+    position: relative;
+    margin: 3px 5px 3px 0;
+    padding: 3px 20px 3px 5px;
+    border: 1px solid #aaa;
+    max-width: 100%;
+    border-radius: 3px;
+    background-color: #eee;
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), to(#eee));
+    background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
+    background-size: 100% 19px;
+    background-repeat: repeat-x;
+    background-clip: padding-box;
+    -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, .05);
+    box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, .05);
+    color: #333;
+    line-height: 13px;
+    cursor: default
+}
+
+.chosen-container-multi .chosen-choices li.search-choice span {
+    word-wrap: break-word
+}
+
+.chosen-container-multi .chosen-choices li.search-choice .search-choice-close {
+    position: absolute;
+    top: 4px;
+    right: 3px;
+    display: block;
+    width: 12px;
+    height: 12px;
+    background: url(chosen-sprite.png) -42px 1px no-repeat;
+    font-size: 1px
+}
+
+.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover {
+    background-position: -42px -10px
+}
+
+.chosen-container-multi .chosen-choices li.search-choice-disabled {
+    padding-right: 5px;
+    border: 1px solid #ccc;
+    background-color: #e4e4e4;
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), to(#eee));
+    background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
+    color: #666
+}
+
+.chosen-container-multi .chosen-choices li.search-choice-focus {
+    background: #d4d4d4
+}
+
+.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close {
+    background-position: -42px -10px
+}
+
+.chosen-container-multi .chosen-results {
+    margin: 0;
+    padding: 0
+}
+
+.chosen-container-multi .chosen-drop .result-selected {
+    display: list-item;
+    color: #ccc;
+    cursor: default
+}
+
+.chosen-container-active .chosen-single {
+    border: 1px solid #5897fb;
+    -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
+    box-shadow: 0 0 5px rgba(0, 0, 0, .3)
+}
+
+.chosen-container-active.chosen-with-drop .chosen-single {
+    border: 1px solid #aaa;
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #eee), color-stop(80%, #fff));
+    background-image: linear-gradient(#eee 20%, #fff 80%);
+    -webkit-box-shadow: 0 1px 0 #fff inset;
+    box-shadow: 0 1px 0 #fff inset
+}
+
+.chosen-container-active.chosen-with-drop .chosen-single div {
+    border-left: none;
+    background: 0 0
+}
+
+.chosen-container-active.chosen-with-drop .chosen-single div b {
+    background-position: -18px 2px
+}
+
+.chosen-container-active .chosen-choices {
+    border: 1px solid #5897fb;
+    -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
+    box-shadow: 0 0 5px rgba(0, 0, 0, .3)
+}
+
+.chosen-container-active .chosen-choices li.search-field input[type=text] {
+    color: #222!important
+}
+
+.chosen-disabled {
+    opacity: .5!important;
+    cursor: default
+}
+
+.chosen-disabled .chosen-single {
+    cursor: default
+}
+
+.chosen-disabled .chosen-choices .search-choice .search-choice-close {
+    cursor: default
+}
+
+.chosen-rtl {
+    text-align: right
+}
+
+.chosen-rtl .chosen-single {
+    overflow: visible;
+    padding: 0 8px 0 0
+}
+
+.chosen-rtl .chosen-single span {
+    margin-right: 0;
+    margin-left: 26px;
+    direction: rtl
+}
+
+.chosen-rtl .chosen-single-with-deselect span {
+    margin-left: 38px
+}
+
+.chosen-rtl .chosen-single div {
+    right: auto;
+    left: 3px
+}
+
+.chosen-rtl .chosen-single abbr {
+    right: auto;
+    left: 26px
+}
+
+.chosen-rtl .chosen-choices li {
+    float: right
+}
+
+.chosen-rtl .chosen-choices li.search-field input[type=text] {
+    direction: rtl
+}
+
+.chosen-rtl .chosen-choices li.search-choice {
+    margin: 3px 5px 3px 0;
+    padding: 3px 5px 3px 19px
+}
+
+.chosen-rtl .chosen-choices li.search-choice .search-choice-close {
+    right: auto;
+    left: 4px
+}
+
+.chosen-rtl.chosen-container-single .chosen-results {
+    margin: 0 0 4px 4px;
+    padding: 0 4px 0 0
+}
+
+.chosen-rtl .chosen-results li.group-option {
+    padding-right: 15px;
+    padding-left: 0
+}
+
+.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div {
+    border-right: none
+}
+
+.chosen-rtl .chosen-search input[type=text] {
+    padding: 4px 5px 4px 20px;
+    background: url(chosen-sprite.png) no-repeat -30px -20px;
+    direction: rtl
+}
+
+.chosen-rtl.chosen-container-single .chosen-single div b {
+    background-position: 6px 2px
+}
+
+.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b {
+    background-position: -12px 2px
+}
+
+@media only screen and (-webkit-min-device-pixel-ratio:1.5),
+only screen and (min-resolution:144dpi),
+only screen and (min-resolution:1.5dppx) {
+    .chosen-container .chosen-results-scroll-down span,
+    .chosen-container .chosen-results-scroll-up span,
+    .chosen-container-multi .chosen-choices .search-choice .search-choice-close,
+    .chosen-container-single .chosen-search input[type=text],
+    .chosen-container-single .chosen-single abbr,
+    .chosen-container-single .chosen-single div b,
+    .chosen-rtl .chosen-search input[type=text] {
+        background-image: url(chosen-sprite@2x.png)!important;
+        background-size: 52px 37px!important;
+        background-repeat: no-repeat!important
+    }
+}

+ 477 - 0
src/assets/css/jsmind.css

@@ -0,0 +1,477 @@
+/*
+ * Released under BSD License
+ * Copyright (c) 2014-2015 hizzgdev@163.com
+ * 
+ * Project Home:
+ *   https://github.com/hizzgdev/jsmind/
+ */
+
+
+/* important section */
+
+.jsmind-inner {
+    position: relative;
+    overflow: auto;
+    width: 100%;
+    height: 100%;
+}
+
+
+/*box-shadow:0 0 2px #000;*/
+
+.jsmind-inner {
+    moz-user-select: -moz-none;
+    -moz-user-select: none;
+    -o-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+
+
+/* z-index:1 */
+
+canvas {
+    position: absolute;
+    z-index: 1;
+}
+
+
+/* z-index:2 */
+
+jmnodes {
+    position: absolute;
+    z-index: 2;
+    background-color: rgba(0, 0, 0, 0);
+}
+
+
+/*background color is necessary*/
+
+jmnode {
+    position: absolute;
+    cursor: default;
+    max-width: 400px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+jmexpander {
+    position: absolute;
+    width: 11px;
+    height: 11px;
+    display: block;
+    overflow: hidden;
+    line-height: 12px;
+    font-size: 12px;
+    text-align: center;
+    border-radius: 6px;
+    border-width: 1px;
+    border-style: solid;
+    cursor: pointer;
+}
+
+
+/* default theme */
+
+jmnode {
+    padding: 10px;
+    background-color: #fff;
+    color: #333;
+    border-radius: 5px;
+    box-shadow: 1px 1px 1px #666;
+    font: 16px/1.125 Verdana, Arial, Helvetica, sans-serif;
+}
+
+jmnode:hover {
+    box-shadow: 2px 2px 8px #000;
+    background-color: #ebebeb;
+    color: #333;
+}
+
+jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+    box-shadow: 2px 2px 8px #000;
+}
+
+jmnode.root {
+    font-size: 24px;
+}
+
+jmexpander {
+    border-color: gray;
+}
+
+jmexpander:hover {
+    border-color: #000;
+}
+
+@media screen and (max-device-width: 1024px) {
+    jmnode {
+        padding: 5px;
+        border-radius: 3px;
+        font-size: 14px;
+    }
+    jmnode.root {
+        font-size: 21px;
+    }
+}
+
+
+/* primary theme */
+
+jmnodes.theme-primary jmnode {
+    background-color: #428bca;
+    color: #fff;
+    border-color: #357ebd;
+}
+
+jmnodes.theme-primary jmnode:hover {
+    background-color: #3276b1;
+    border-color: #285e8e;
+}
+
+jmnodes.theme-primary jmnode.selected {
+    background-color: #f1c40f;
+    color: #fff;
+}
+
+jmnodes.theme-primary jmnode.root {}
+
+jmnodes.theme-primary jmexpander {}
+
+jmnodes.theme-primary jmexpander:hover {}
+
+
+/* warning theme */
+
+jmnodes.theme-warning jmnode {
+    background-color: #f0ad4e;
+    border-color: #eea236;
+    color: #fff;
+}
+
+jmnodes.theme-warning jmnode:hover {
+    background-color: #ed9c28;
+    border-color: #d58512;
+}
+
+jmnodes.theme-warning jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-warning jmnode.root {}
+
+jmnodes.theme-warning jmexpander {}
+
+jmnodes.theme-warning jmexpander:hover {}
+
+
+/* danger theme */
+
+jmnodes.theme-danger jmnode {
+    background-color: #d9534f;
+    border-color: #d43f3a;
+    color: #fff;
+}
+
+jmnodes.theme-danger jmnode:hover {
+    background-color: #d2322d;
+    border-color: #ac2925;
+}
+
+jmnodes.theme-danger jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-danger jmnode.root {}
+
+jmnodes.theme-danger jmexpander {}
+
+jmnodes.theme-danger jmexpander:hover {}
+
+
+/* success theme */
+
+jmnodes.theme-success jmnode {
+    background-color: #5cb85c;
+    border-color: #4cae4c;
+    color: #fff;
+}
+
+jmnodes.theme-success jmnode:hover {
+    background-color: #47a447;
+    border-color: #398439;
+}
+
+jmnodes.theme-success jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-success jmnode.root {}
+
+jmnodes.theme-success jmexpander {}
+
+jmnodes.theme-success jmexpander:hover {}
+
+
+/* info theme */
+
+jmnodes.theme-info jmnode {
+    background-color: #5dc0de;
+    border-color: #46b8da;
+    ;
+    color: #fff;
+}
+
+jmnodes.theme-info jmnode:hover {
+    background-color: #39b3d7;
+    border-color: #269abc;
+}
+
+jmnodes.theme-info jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-info jmnode.root {}
+
+jmnodes.theme-info jmexpander {}
+
+jmnodes.theme-info jmexpander:hover {}
+
+
+/* greensea theme */
+
+jmnodes.theme-greensea jmnode {
+    background-color: #1abc9c;
+    color: #fff;
+}
+
+jmnodes.theme-greensea jmnode:hover {
+    background-color: #16a085;
+}
+
+jmnodes.theme-greensea jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-greensea jmnode.root {}
+
+jmnodes.theme-greensea jmexpander {}
+
+jmnodes.theme-greensea jmexpander:hover {}
+
+
+/* nephrite theme */
+
+jmnodes.theme-nephrite jmnode {
+    background-color: #2ecc71;
+    color: #fff;
+}
+
+jmnodes.theme-nephrite jmnode:hover {
+    background-color: #27ae60;
+}
+
+jmnodes.theme-nephrite jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-nephrite jmnode.root {}
+
+jmnodes.theme-nephrite jmexpander {}
+
+jmnodes.theme-nephrite jmexpander:hover {}
+
+
+/* belizehole theme */
+
+jmnodes.theme-belizehole jmnode {
+    background-color: #3498db;
+    color: #fff;
+}
+
+jmnodes.theme-belizehole jmnode:hover {
+    background-color: #2980b9;
+}
+
+jmnodes.theme-belizehole jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-belizehole jmnode.root {}
+
+jmnodes.theme-belizehole jmexpander {}
+
+jmnodes.theme-belizehole jmexpander:hover {}
+
+
+/* wisteria theme */
+
+jmnodes.theme-wisteria jmnode {
+    background-color: #9b59b6;
+    color: #fff;
+}
+
+jmnodes.theme-wisteria jmnode:hover {
+    background-color: #8e44ad;
+}
+
+jmnodes.theme-wisteria jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-wisteria jmnode.root {}
+
+jmnodes.theme-wisteria jmexpander {}
+
+jmnodes.theme-wisteria jmexpander:hover {}
+
+
+/* asphalt theme */
+
+jmnodes.theme-asphalt jmnode {
+    background-color: #34495e;
+    color: #fff;
+}
+
+jmnodes.theme-asphalt jmnode:hover {
+    background-color: #2c3e50;
+}
+
+jmnodes.theme-asphalt jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-asphalt jmnode.root {}
+
+jmnodes.theme-asphalt jmexpander {}
+
+jmnodes.theme-asphalt jmexpander:hover {}
+
+
+/* orange theme */
+
+jmnodes.theme-orange jmnode {
+    background-color: #f1c40f;
+    color: #fff;
+}
+
+jmnodes.theme-orange jmnode:hover {
+    background-color: #f39c12;
+}
+
+jmnodes.theme-orange jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-orange jmnode.root {}
+
+jmnodes.theme-orange jmexpander {}
+
+jmnodes.theme-orange jmexpander:hover {}
+
+
+/* pumpkin theme */
+
+jmnodes.theme-pumpkin jmnode {
+    background-color: #e67e22;
+    color: #fff;
+}
+
+jmnodes.theme-pumpkin jmnode:hover {
+    background-color: #d35400;
+}
+
+jmnodes.theme-pumpkin jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-pumpkin jmnode.root {}
+
+jmnodes.theme-pumpkin jmexpander {}
+
+jmnodes.theme-pumpkin jmexpander:hover {}
+
+
+/* pomegranate theme */
+
+jmnodes.theme-pomegranate jmnode {
+    background-color: #e74c3c;
+    color: #fff;
+}
+
+jmnodes.theme-pomegranate jmnode:hover {
+    background-color: #c0392b;
+}
+
+jmnodes.theme-pomegranate jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-pomegranate jmnode.root {}
+
+jmnodes.theme-pomegranate jmexpander {}
+
+jmnodes.theme-pomegranate jmexpander:hover {}
+
+
+/* clouds theme */
+
+jmnodes.theme-clouds jmnode {
+    background-color: #ecf0f1;
+    color: #333;
+}
+
+jmnodes.theme-clouds jmnode:hover {
+    background-color: #bdc3c7;
+}
+
+jmnodes.theme-clouds jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-clouds jmnode.root {}
+
+jmnodes.theme-clouds jmexpander {}
+
+jmnodes.theme-clouds jmexpander:hover {}
+
+
+/* asbestos theme */
+
+jmnodes.theme-asbestos jmnode {
+    background-color: #95a5a6;
+    color: #fff;
+}
+
+jmnodes.theme-asbestos jmnode:hover {
+    background-color: #7f8c8d;
+}
+
+jmnodes.theme-asbestos jmnode.selected {
+    background-color: #11f;
+    color: #fff;
+}
+
+jmnodes.theme-asbestos jmnode.root {}
+
+jmnodes.theme-asbestos jmexpander {}
+
+jmnodes.theme-asbestos jmexpander:hover {}

File diff suppressed because it is too large
+ 2 - 0
src/assets/image/uncultivated.svg


BIN
src/assets/logo.png


File diff suppressed because it is too large
+ 489 - 0
src/assets/style/iconfont/iconfont.css


BIN
src/assets/style/iconfont/iconfont.eot


File diff suppressed because it is too large
+ 380 - 0
src/assets/style/iconfont/iconfont.svg


BIN
src/assets/style/iconfont/iconfont.ttf


BIN
src/assets/style/iconfont/iconfont.woff


+ 100 - 0
src/assets/style/style.scss

@@ -0,0 +1,100 @@
+body{
+    /deep/ .el-notification.right{
+        text-align: left;
+    }
+}
+
+.icon-wushuju {
+    display: block;
+    width: 100px;
+    height: 100px;
+    font-size: 50px !important;
+    margin: auto;
+}
+
+.right {
+    text-align: right;
+    background: #fff;
+    padding-top: 10px;
+}
+
+.left {
+    text-align: left;
+    background: #fff;
+    padding-top: 10px;
+}
+
+.right-nobg {
+    text-align: right;
+}
+
+.el-loading-mask {
+    z-index: 499;
+}
+
+.search-title {
+    height: 53px;
+    border: 1px solid #dfe6ec;
+    background-color: #fff;
+    padding: 10px 10px 10px 20px;
+    box-sizing: border-box;
+    margin-bottom: 10px;
+    /deep/ .el-input__inner{
+        vertical-align: baseline;
+    }
+}
+
+.p12 {
+    padding-right: 12px;
+    color: #999;
+}
+
+iframe {
+    border: none;
+}
+
+.el-button--success.is-plain,
+.el-button--primary.is-plain,
+.el-button--info.is-plain,
+.el-button--warning.is-plain,
+.el-button--danger.is-plain {
+    background-color: #fff;
+}
+
+.el-dropdown-link {
+    cursor: pointer;
+    color: #409eff;
+}
+
+.middle_sty {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.el-table__empty-block {
+    align-items: initial;
+}
+
+.p10 {
+    padding: 0 10px;
+    box-sizing: border-box;
+}
+
+.el-checkbox__label{
+    font-size: 12px;
+}
+jmnodes.theme-greensea jmnode {
+    color: #fff;
+    background-color: #67c23a;
+    border-color: #67c23a;
+}
+
+// jmnodes.theme-greensea jmnode.selected {
+//     background: #85ce61;
+//     border-color: #85ce61;
+// }
+// jmnodes.theme-greensea jmnode:hover {
+//     background: #85ce61;
+//     border-color: #85ce61;
+// }

+ 84 - 0
src/components/dasboard/index.vue

@@ -0,0 +1,84 @@
+<!--
+ * @Author: zhangyu
+ * @Date: 2020-05-26 15:05:21
+ * @Info: 
+ * @LastEditTime: 2020-05-26 15:06:01
+--> 
+<template>
+  <div class="main-box">
+    <div class="mian-icon">
+      <img src="@/assets/image/uncultivated.svg">
+    </div>
+    <div class="main-text">
+      <div class="main-content">
+        <h1>功能设计中。。。</h1>
+        <p class="time"><b>计划完成:</b>
+          <slot name="plan"></slot><b>开发完成:</b>
+          <slot name="finish"></slot>
+        </p>
+        <p><b>上线时间:</b>
+          <slot name="onLine"></slot>
+        </p>
+        <p class="explain">业务功能说明:</p>
+        <div>
+          <slot name="explain"></slot>
+        </div>
+      </div>
+    </div>
+  </div>
+  <!-- <h4>开发中...</h4> -->
+  <!-- <button v-if="hasPermission('system:role:query')">测试权限 </button> -->
+</template>
+<script>
+export default {
+  name: 'Dasboard',
+  props: [],
+  data() {
+    return {}
+  },
+  computed: {},
+  methods: {},
+  created() {
+    // console.log('--------------------- index created')
+  },
+  mounted() { },
+  components: {}
+}
+</script>
+<style lang='scss' scoped>
+.main-box {
+  width: 100%;
+  height: 100%;
+  display: flex;
+}
+.mian-icon {
+  flex: 2;
+  text-align: center;
+  position: relative;
+  img {
+    position: absolute;
+    top: 25%;
+  }
+}
+.main-text {
+  flex: 3;
+  position: relative;
+  .main-content {
+    position: absolute;
+    padding-right: 200px;
+    top: 20%;
+    .time {
+      margin-top: 10px;
+    }
+    .explain {
+      margin-top: 20px;
+    }
+    p {
+      font-weight: bold;
+      span {
+        margin-right: 20px;
+      }
+    }
+  }
+}
+</style>

+ 326 - 0
src/data/menus.js

@@ -0,0 +1,326 @@
+export default [
+	/******************** 平台管理 ***************************/
+	{
+		path: '/platform',
+		name: '平台管理',
+		disabled: true,
+	},
+	// 项目管理
+	{
+		path: '/platform/project',
+		name: '项目管理',
+		icon: 'icon-project-o'
+	},
+	// 人员管理
+	{
+		path: '/platform/user',
+		name: '人员管理',
+		icon: 'icon-renyuanguanli',
+		opts: [{
+			name: '查看',
+			basic: true,
+			permission: 'system:user:query'
+		}]
+	},
+	// 角色管理
+	{
+		path: '/platform/role',
+		name: '角色管理',
+		icon: 'icon-jiaoseguanli',
+		opts: [{
+			name: '查看',
+			basic: true,
+			permission: 'system:role:query'
+		}]
+	},
+	/******************** 前期准备 ***************************/
+	{
+		path: '/ready',
+		name: '前期准备',
+		disabled: true,
+	},
+	// 建筑楼层管理
+	{
+		path: '/ready/buildfloor',
+		name: '建筑楼层管理',
+		icon: 'icon-jianzhu'
+	},
+	// 需采集的信息点
+	{
+		path: '/ready/collectsetting',
+		name: '需采集的信息点',
+		icon: 'icon-xinxi'
+	},
+	// 扫楼App用户管理
+	{
+		path: '/ready/appuser',
+		name: '扫楼App用户管理',
+		icon: 'icon-app4'
+	},
+	/******************** 信息收集 ***************************/
+	{
+		path: '/information',
+		name: '信息收集',
+		disabled: true,
+	},
+	// 模型管理
+	{
+		path: '/model',
+		name: '模型管理',
+		icon: 'icon-moxing___',
+		children: [{
+			path: '/model/file',
+			name: '模型文件管理',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}, {
+			path: '/model/report',
+			name: '模型质量报告',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}]
+	},
+	// 系统集成
+	{
+		path: '/point',
+		name: '系统集成',
+		icon: 'icon-xitong',
+		children: [{
+			path: '/point/pointsetting',
+			name: '子系统点位接入',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}, {
+			path: '/point/dynamicdata',
+			name: '配置动参从点位取值',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		},
+		// {
+		// 	path: '/point/objectdata',
+		// 	name: '配置动参从对象取值',
+		// 	icon: '',
+		// 	opts: [{
+		// 		name: '查看',
+		// 		basic: true,
+		// 		permission: 'system:role:query'
+		// 	}]
+		// },
+		{
+			path: '/point/report',
+			name: '系统集成成果管理',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}]
+	},
+	// 扫楼作业
+	{
+		path: '/floor',
+		name: '扫楼作业',
+		icon: 'icon-shanglou',
+		children: [{
+			path: '/floor/task',
+			name: '现场任务管理',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		},{
+			path: '/floor/data',
+			name: '现场数据整理',
+			icon: '',
+			children: [ {
+				path: '/floor/data',
+				name: '信息点整理',
+				icon: '',
+				opts: [{
+					name: '查看',
+					basic: true,
+					permission: 'system:role:query'
+				}]
+			},
+			{
+				path: '/floor/plan',
+				name: '位置标签整理',
+				icon: '',
+				opts: [{
+					name: '查看',
+					basic: true,
+					permission: 'system:role:query'
+				}]
+			}]
+		}, {
+			path: '/floor/abnormalprop',
+			name: '扫楼报告',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}, {
+			path: '/floor/log',
+			name: '扫楼日志查看',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}]
+	},
+	// 台账管理
+	{
+		path: '/ledger',
+		name: '台账管理',
+		icon: 'icon-taizhangzhangbushezhi',
+		children: [{
+			path: '/ledger/facility',
+			name: '设备台账',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}, {
+			path: '/ledger/property',
+			name: '资产台账',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}, {
+			path: '/ledger/list',
+			name: '系统台账',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}, {
+			path: '/ledger/spacelist',
+			name: '业务空间台账',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}, {
+				path: '/ledger/cenotelist',
+				name: '竖井台账',
+				icon: '',
+				opts: [{
+					name: '查看',
+					basic: true,
+					permission: 'system:role:query'
+				}]
+		},{
+			path: '/ledger/rentlist',
+			name: '租户台账',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}, {
+			path: '/ledger/datareport',
+			name: '数据质量报告',
+			icon: '',
+			opts: [{
+				name: '查看',
+				basic: true,
+				permission: 'system:role:query'
+			}]
+		}]
+	},
+	/******************** 关系维护并计算 ***************************/
+	{
+		path: '/relation',
+		name: '关系维护并计算',
+		disabled: true,
+	},
+	// 关系维护
+	// {
+	// 	path: '/relation/maintain',
+	// 	name: '关系维护',
+	// 	icon: 'icon-guanxi',
+	// 	opts: [{
+	// 		name: '查看',
+	// 		basic: true,
+	// 		permission: 'system:user:query'
+	// 	}]
+	// },
+	// 全部关系总览
+	{
+		path: '/relation/overview',
+		name: '全部关系总览',
+		icon: 'icon-jiqixuexi-',
+		opts: [{
+			name: '查看',
+			basic: true,
+			permission: 'system:role:query'
+		}]
+	},
+	/******************** 数据项目化交付 ***************************/
+	{
+		path: '/deliver',
+		name: '数据项目化交付',
+		disabled: true,
+	},
+	// 项目数据转换
+	{
+		path: '/relation/data',
+		name: '项目数据转换',
+		icon: 'icon-view',
+		opts: [{
+			name: '查看',
+			basic: true,
+			permission: 'system:role:query'
+		}]
+	},
+	/******************** 通用字典 ***************************/
+	// {
+	// 	path: '/dictionaries',
+	// 	name: '通用字典',
+	// 	disabled: true,
+	// },
+	// // 厂家库
+	// {
+	// 	path: '/manufactor/supplier',
+	// 	name: '厂家库',
+	// 	icon: '',
+	// 	opts: [{
+	// 		name: '查看',
+	// 		basic: true,
+	// 		permission: 'system:role:query'
+	// 	}]
+	// }
+]

+ 428 - 0
src/framework/components/messagesever/index.vue

@@ -0,0 +1,428 @@
+<!--
+ * @Author: zhangyu
+ * @Date: 2019-08-26 15:22:13
+ * @Info: 
+ * @LastEditTime: 2020-05-26 15:37:36
+ -->
+<template>
+  <div class="notification-box" v-clickOutside="handleClose" @click="handleClickRead">
+    <el-badge :value="unreadNum" :hidden="!unreadNum" :max="99">
+      <i class="el-icon-message-solid"></i>
+    </el-badge>
+    <transition name="el-fade-in-linear">
+      <div v-show="noticeListShow" @click.stop="" class="noticeBox">
+        <ul class="noticeTab">
+          <li class="noticeTab_item">消息通知
+            <!-- <span class="unread_num">1</span> -->
+          </li>
+        </ul>
+        <div class="noticeList_scroll">
+          <el-scrollbar style="height:100%;">
+            <ul class="noticeList">
+              <li class="noticeList_item" v-for="item in messageList" :key="item.Id" :title="item.Content.Message?item.Content.Message:''">
+                <div class="noticeItem_box">
+                  <div class="noticeItem_text">
+                    <i :class="iconClassMap[item.Type]?iconClassMap[item.Type]:'msg-icon el-icon-warning warning-color'"></i>
+                    <p><span>{{`【${moduleMap[item.Module]?moduleMap[item.Module]:item.Module}】 `}}</span>{{item.Title?item.Title:""}}</p>
+                  </div>
+                  <div class="noticeItem_time">
+                    <span class="proname">{{item.Project[0].ProjLocalName || ''}}</span>
+                    {{item.CreateTime}}
+                    <el-link v-for="(btn, index) in item.Content.ButtonList?item.Content.ButtonList:[]"
+                      style="float:right;font-size:12px;margin-left:5px;" type="primary" :key="index"
+                      :href="`/image-service/common/file_get?systemId=revit&key=${btn.Url}`" :download="btn.FileName?btn.FileName:''">
+                      {{btn.Name?btn.Name:""}}</el-link>
+                  </div>
+                </div>
+              </li>
+            </ul>
+          </el-scrollbar>
+        </div>
+        <div class="notice_operate">
+          <!-- <div class="readAll">全部标为已读</div> -->
+          <div class="seeAll" @click="allDetails">查看全部<i class="iconfont icon-right"></i></div>
+        </div>
+      </div>
+    </transition>
+    <!-- <el-button type="primary" @click="handleClickConnectMQ">连接MQ</el-button>
+    <el-button type="primary" @click="handleClickDisconnectMQ">断开MQ</el-button>
+    <el-button type="primary" @click="handleClickUnsubscribe">停止接收消息</el-button><br><br>
+    <el-input type="textarea" :autosize="{ minRows: 6, maxRows: 6}" v-model="sendMessage" placeholder="请输入要发送的内容"></el-input>
+    <br><br>
+    <el-button type="primary" @click="handleClickSendMessage">发送消息</el-button>
+    <el-card style="margin-top:20px;text-align:left;">
+      <div slot="header" class="clearfix">
+        <span>消息历史</span>
+      </div>
+      <div v-for="(message, index) in messageList" :key="index" class="text item">
+        {{ message }}
+      </div>
+    </el-card> -->
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Stomp from 'stompjs'
+import Msmq from './msmq'
+import Bus from '@/utils/bus.js'
+import { MQTT_SERVICE, MQTT_USERNAME, MQTT_PASSWORD } from './mqSetting'
+// import { messgeCount, messgeQuery, messgeUpdateState } from '@/api/msgsever'
+
+export default {
+  components: {
+
+  },
+  data() {
+    return {
+      client: Stomp.client(MQTT_SERVICE),
+      unreadNum: 0,
+      noticeListShow: false,//是否显示消息列表
+      sendMessage: '',
+      messageList: [], // 历史消息
+      subList: [], // 订阅的消息实例
+      topics: ["/topic/message.manage"], // 订阅的消息名称
+      messageList: [],
+      moduleMap: {//消息模块映射
+        "Model": "模型文件管理"
+      },
+      iconClassMap: { //消息类型图标映射
+        "Success": "msg-icon el-icon-success success-color",
+        "Error": "msg-icon el-icon-error error-color",
+        "Warning": "msg-icon el-icon-warning warning-color",
+        "Info": "msg-icon el-icon-info info-color",
+      },
+    }
+  },
+  created() {
+    this.connect()
+  },
+  mounted() {
+    // this.getUnreadCount()
+    // this.getMessageList()//获取消息列表
+    // Bus.$on('messageListUpdate', message => {
+    //   this.getMessageList()
+    // })
+    // Bus.$on('getUnreadCountUpdate', message => {
+    //   this.getUnreadCount()
+    // })
+  },
+  computed: {
+    ...mapGetters('layout', ['userInfo', 'projectId', 'projects', 'userId'])
+  },
+  methods: {
+    getMessageList() {//获取消息列表(最新10条)
+      let params = {
+        Cascade: [
+          {
+            Name: "project"
+          }
+        ],
+        Filters: `UserId='${this.userId}';Type!='Refresh'`,
+        Orders: "CreateTime desc, Id asc",
+        PageNumber: 1,
+        PageSize: 10
+      }
+      messgeQuery(params, res => {
+        this.messageList = res.Content
+      })
+    },
+    getUnreadCount() {//获取未读消息的数量
+      let params = {
+        Filters: `Read=false;UserId='${this.userId}';Type!='Refresh'`
+      }
+      messgeCount(params, res => {
+        this.unreadNum = res.Count
+      })
+    },
+    setAllRead() {//将当前角色消息全部置为已读
+      if (this.userId) {
+        messgeUpdateState({ Read: true, UserId: this.userId }, res => { this.unreadNum = 0 })
+      }
+    },
+    handleClose(e) {//关闭消息列表
+      this.noticeListShow = false
+    },
+    handleClickRead() {//点击消息铃铛
+      this.noticeListShow ? this.noticeListShow = false : this.noticeListShow = true
+      if (this.noticeListShow) {
+        this.setAllRead()//将当前角色消息全部置为已读
+      }
+    },
+    connect() {
+      this.client = Stomp.client(MQTT_SERVICE)
+      this.client.reconnect_delay = 5000
+      var clientid = `sagaMQ-${this.userInfo.userName}-${new Date().getTime()}`
+      var headers = {
+        'login': MQTT_USERNAME,
+        'passcode': MQTT_PASSWORD,
+        'client-id': clientid
+      }
+      this.client.connect(headers, this.onConnected, this.onFailed)
+    },
+    onConnected(frame) {
+      console.log('Connected: ' + frame)
+      //订阅多个消息
+      this.topics.forEach(item => {
+        let sub = this.client.subscribe(item, this.onmessage, this.onFailed)
+        this.subList.push(sub)
+      })
+      // this.client.subscribe(topic, this.onmessage, this.onFailed) 
+    },
+    //接收到消息的回调
+    onmessage(message) {
+      this.unreadNum = Msmq.handleMsg(message, this.projects, this.userId, this.unreadNum);
+    },
+    // 接收消息失败回调
+    onFailed(frame) {
+      console.log('Failed: ' + frame)
+    },
+    //停止接收消息
+    unsubscribe() {
+      this.subList.forEach((item) => {
+        item.unsubscribe()
+      })
+    },
+    //断开连接
+    disconnect() {
+      this.client.disconnect(function () {
+        console.log("连接已断开!");
+      })
+    },
+    //发送消息
+    send(destination, message, headers = {}) {
+      this.client.send(destination, headers, JSON.stringify(message))
+    },
+
+    handleClickConnectMQ() {//连接MQ
+      this.connect()
+    },
+    handleClickDisconnectMQ() {//断开MQ
+      this.disconnect()
+    },
+    handleClickUnsubscribe() {//停止接收消息
+      this.unsubscribe()
+    },
+    handleClickSendMessage() {//发送消息
+      this.send('/topic/datacenter.broadcast', this.sendMessage)
+    },
+    allDetails() {
+      this.noticeListShow = false;
+      this.$router.push({ path: "/allDetails" });
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.notification-box {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  padding: 16px 10px 10px;
+  box-sizing: border-box;
+  color: #79869a;
+  cursor: pointer;
+  .noticeBox {
+    position: absolute;
+    top: 50px;
+    left: 50%;
+    margin-left: -220px;
+    cursor: auto;
+    z-index: 1000;
+    text-align: left;
+    width: 272px;
+    height: 362px;
+    box-shadow: 0 1px 6px 0 #ccc;
+    color: #333;
+    background: #fff;
+    overflow: visible !important;
+  }
+  .noticeBox::before {
+    content: "";
+    border: solid 7px transparent;
+    border-bottom-color: #fff;
+    display: block;
+    position: absolute;
+    width: 2px;
+    top: -14px;
+    left: 50%;
+    margin-left: 77px;
+    z-index: 0;
+  }
+  .noticeTab {
+    padding: 8px 16px 0;
+    z-index: 10;
+    height: 32px;
+    zoom: 1;
+    box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.1) inset;
+    .noticeTab_item {
+      float: left;
+      position: relative;
+      width: 240px;
+      height: 32px;
+      line-height: 32px;
+      padding: 0;
+      margin-right: 0;
+      font-weight: 700;
+      text-align: left;
+      cursor: pointer;
+      -webkit-transition: all 0.2s ease-in-out;
+      -o-transition: all 0.2s ease-in-out;
+      transition: all 0.2s ease-in-out;
+      .unread_num {
+        display: inline-block;
+        color: #fff;
+        background-color: #5182e4;
+        height: 14px;
+        padding: 0 2px;
+        line-height: 14px;
+        text-align: center;
+        border-radius: 2px;
+        font-size: 12px;
+        padding: 0 3px;
+        font-family: Arial;
+        -webkit-transform: scale(0.85);
+        -ms-transform: scale(0.85);
+        -o-transform: scale(0.85);
+        transform: scale(0.85);
+      }
+    }
+    .active {
+      box-shadow: 0 -2px 0 0 #5182e4 inset;
+    }
+  }
+  .noticeList_scroll {
+    height: 282px;
+    /deep/ .el-scrollbar__wrap {
+      overflow-x: hidden;
+    }
+    .noticeList_item {
+      width: 100%;
+      height: auto;
+      position: relative;
+      cursor: pointer;
+      box-sizing: border-box;
+      padding: 12px 16px;
+      -webkit-transition: all 0.2s ease-in-out;
+      -o-transition: all 0.2s ease-in-out;
+      transition: all 0.2s ease-in-out;
+      zoom: 1;
+      .noticeItem_box {
+        float: left;
+        position: relative;
+        width: 100%;
+        .noticeItem_text {
+          color: rgba(10, 18, 32, 0.87);
+          font-weight: 700;
+          padding-left: 18px;
+          text-indent: -6px;
+          position: relative;
+          font-size: 12px;
+          line-height: 17px;
+          -webkit-transition: all 0.2s ease-in-out;
+          -o-transition: all 0.2s ease-in-out;
+          transition: all 0.2s ease-in-out;
+          .msg-icon {
+            position: absolute;
+            top: 2.5px;
+            left: 6px;
+          }
+          p {
+            word-break: break-all;
+          }
+        }
+        .noticeItem_time {
+          font-size: 12px;
+          height: 19px;
+          line-height: 19px;
+          color: rgba(10, 18, 32, 0.46);
+          margin-top: 4px;
+          padding-left: 18px;
+          .proname{
+            float: left;
+            display: block;
+            width: 80px;
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+          }
+        }
+      }
+    }
+    .noticeList_item:hover {
+      background-color: #f5f7f7;
+    }
+    .noticeList_item:after {
+      content: "";
+      display: block;
+      height: 0;
+      clear: both;
+      visibility: hidden;
+    }
+    .noticeList_item:before {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      height: 1px;
+      padding: 0 16px;
+      background-color: #ebebeb;
+      content: "";
+      background-clip: content-box;
+    }
+  }
+  .notice_operate {
+    height: 40px;
+    line-height: 20px;
+    padding: 10px 16px;
+    box-sizing: border-box;
+    background-color: rgba(242, 243, 245, 0.48);
+    box-shadow: inset 0 1px 0 0 rgba(10, 18, 32, 0.06);
+    zoom: 1;
+    .readAll {
+      float: left;
+      cursor: pointer;
+      -webkit-transition: all 0.2s ease-in-out;
+      -o-transition: all 0.2s ease-in-out;
+      transition: all 0.2s ease-in-out;
+    }
+    .readAll:hover {
+      text-decoration: underline;
+    }
+    .seeAll {
+      float: right;
+      color: #5182e4;
+      cursor: pointer;
+      font-weight: 700;
+    }
+    .seeAll:hover {
+      text-decoration: underline;
+    }
+  }
+  .el-icon-message-solid {
+    font-size: 22px;
+  }
+  /deep/ .el-badge__content {
+    height: 16px;
+    line-height: 16px;
+    border: 1px solid transparent;
+  }
+}
+.notification-box:hover {
+  color: #d3d8e2;
+  background-color: #3f4f62;
+}
+.success-color {
+  color: #67c23a;
+}
+.error-color {
+  color: #f56c6c;
+}
+.warning-color {
+  color: #e6a23c;
+}
+.info-color {
+  color: #909399;
+}
+</style>

+ 9 - 0
src/framework/components/messagesever/mqSetting.js

@@ -0,0 +1,9 @@
+/*
+ * @Author: zhangyu
+ * @Date: 2019-08-26 16:03:46
+ * @Info: 
+ * @LastEditTime: 2019-09-05 14:46:30
+ */
+export const MQTT_SERVICE = process.env.MQTT_SERVICE // mq服务地址
+export const MQTT_USERNAME = 'admin' // mq连接用户名
+export const MQTT_PASSWORD = 'admin' // mq连接密码

+ 43 - 0
src/framework/components/messagesever/msmq.js

@@ -0,0 +1,43 @@
+import { Notification } from 'element-ui';
+import Bus from '@/utils/bus.js'
+
+const MSMQ = {
+  msgTypeMap: {
+    "Success": "success",
+    "Error": "error",
+    "Warning": "warning",
+    "Info": "info",
+  },
+  moduleMap: {//消息模块映射
+    "Model": "模型文件管理"
+  },
+  handleMsg (message, projects, userId, unreadNum ) {
+    let data
+    try {
+      data = JSON.parse(message.body)
+    } catch (err) {
+      return false
+    }
+    if(data.ProjectId && projects.some((item) => {return item.id == data.ProjectId})) { //判断消息是否是当前用户拥有的项目
+      if(!data.UserList || (userId && data.UserList && data.UserList.includes(userId))){//判断消息接收人是否是当前用户
+        if(data.Module && data.Module == 'Model'){ //模型文件的消息触发模型文件列表刷新事件
+          Bus.$emit('modelStatusChange', data)
+        }
+        if(data.Type != 'Refresh'){//消息列表可见消息(注类型为'Refresh'的消息用来驱动对应服务刷新,消息中心[用户]不可见)
+          Notification({ 
+            title: `${data.Title?data.Title:''}`, 
+            dangerouslyUseHTMLString: true, 
+            message: `【${this.moduleMap[data.Module]?this.moduleMap[data.Module]:data.Module}】${data.Content?data.Content.Message?data.Content.Message:'':''}`,
+            type: this.msgTypeMap[data.Type]?this.msgTypeMap[data.Type]:'info'
+          })
+          // Notification.info({ title: '消息',dangerouslyUseHTMLString: true, message: `项目编号:${data.projectId}<br>消息类型:${data.type}<br>描述信息:${data.description}<br>` })
+          unreadNum++
+          Bus.$emit('messageListUpdate', data)//消息驱动消息列表刷新
+        }
+        return unreadNum
+      }
+    }
+  }
+}
+
+export default MSMQ

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

@@ -0,0 +1,182 @@
+<template>
+    <div class='login-container'>
+        <el-form
+            class='card-box login-form'
+            autocomplete='on'
+            :model='loginForm'
+            :rules='loginRules'
+            ref='loginForm'
+            label-position='left'
+        >
+            <h3 class='title'>系统登录</h3>
+            <el-form-item prop='username'>
+                <span class='svg-container svg-container_login'>
+                    <i class='el-icon-fa-user-o'></i>
+                </span>
+                <el-input name='username' type='text' v-model='loginForm.username' autocomplete='on' placeholder='邮箱'/>
+            </el-form-item>
+            <el-form-item prop='password'>
+                <span class='svg-container'>
+                    <i class='el-icon-fa-key'></i>
+                </span>
+                <el-input
+                    name='password'
+                    type='password'
+                    v-model='loginForm.password'
+                    placeholder='密码'
+                    @keyup.enter.native.prevent='handleLogin'
+                />
+            </el-form-item>
+            <el-button
+                type='primary'
+                style='width:100%;margin-bottom:30px;'
+                :loading='loading'
+                @click.native.prevent='handleLogin'
+            >登录</el-button>
+        </el-form>
+    </div>
+</template>
+<script>
+function isvalidUsername(val) {
+    return true
+}
+import frameworkApi from '@/api/framework'
+import { mapActions, mapGetters } from 'vuex'
+export default {
+    components: {},
+    name: 'login',
+    data() {
+        const validateUsername = (rule, value, callback) => {
+            if (!isvalidUsername(value)) {
+                callback(new Error('请输入正确的用户名'))
+            } else {
+                callback()
+            }
+        }
+        const validatePassword = (rule, value, callback) => {
+            if (value.length < 6) {
+                callback(new Error('密码不能小于6位'))
+            } else {
+                callback()
+            }
+        }
+        return {
+            loginForm: {
+                username: 'admin',
+                password: '123456'
+            },
+            loginRules: {
+                username: [
+                    {
+                        required: true,
+                        trigger: 'blur',
+                        validator: validateUsername
+                    }
+                ],
+                password: [
+                    {
+                        required: true,
+                        trigger: 'blur',
+                        validator: validatePassword
+                    }
+                ]
+            },
+            loading: false
+        }
+    },
+    computed: {
+        //...mapGetters(['beforeUrl'])
+    },
+    methods: {
+        async handleLogin() {
+            let vm = this
+            vm.$refs.loginForm.validate(valid => {
+                if (valid) {
+                    let username = this.loginForm.username
+                    let password = this.loginForm.password
+                    frameworkApi.login(username, password).then(resp => {
+                        console.log('login result => ', resp)
+                        if (resp.result == 'success') {
+                            this.$store.dispatch('layout/loadUserInfo').then(resp => {
+                                console.log('store dispatch result ', resp)
+                                if (resp.result == 'success') {
+                                    vm.$router.push('/demo/table', () => {})
+                                } else {
+                                }
+                            })
+                        } else {
+                        }
+                    })
+                } else {
+                    console.log('error submit!!')
+                    return false
+                }
+            })
+        }
+    }
+}
+</script>
+<style rel="stylesheet/scss" lang="scss">
+$bg: #2d3a4b;
+$dark_gray: #889aa4;
+$light_gray: #eee;
+.login-container {
+    height: 100vh;
+    background-color: $bg;
+    input:-webkit-autofill {
+        -webkit-box-shadow: 0 0 0px 1000px #293444 inset !important;
+        -webkit-text-fill-color: #fff !important;
+    }
+    input {
+        background: transparent;
+        border: 0px;
+        -webkit-appearance: none;
+        border-radius: 0px;
+        padding: 12px 5px 12px 15px;
+        color: $light_gray;
+        height: 47px;
+    }
+    .el-input {
+        display: inline-block;
+        height: 47px;
+        width: 85%;
+    }
+    .tips {
+        font-size: 14px;
+        color: #fff;
+        margin-bottom: 10px;
+    }
+    .svg-container {
+        padding: 6px 5px 6px 15px;
+        color: $dark_gray;
+        vertical-align: middle;
+        width: 30px;
+        display: inline-block;
+        &_login {
+            font-size: 20px;
+        }
+    }
+    .title {
+        font-size: 26px;
+        font-weight: 400;
+        color: $light_gray;
+        margin: 0px auto 40px auto;
+        text-align: center;
+        font-weight: bold;
+    }
+    .login-form {
+        position: absolute;
+        left: 0;
+        right: 0;
+        width: 400px;
+        padding: 35px 35px 15px 35px;
+        margin: 120px auto;
+    }
+    .el-form-item {
+        border: 1px solid rgba(255, 255, 255, 0.1);
+        background: rgba(0, 0, 0, 0.1);
+        border-radius: 5px;
+        color: #454545;
+    }
+}
+</style>

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

@@ -0,0 +1,87 @@
+<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 v-show="!breadcrumb.length"  :to='{ path: "/" }'>首页</el-breadcrumb-item>
+                    <el-breadcrumb-item v-show="breadcrumb.length" v-for='(b, index) in breadcrumb' :key='index' :to='b.path ? { path: b.path } : null'>{{b.label}}</el-breadcrumb-item>                       
+                    </el-breadcrumb>
+                </div>
+                <!-- <router-view class='page-content'/> -->
+                <keep-alive>
+                    <router-view v-if='$route.meta.keepAlive' class='page-content'></router-view>
+                </keep-alive>
+                <router-view v-if='!$route.meta.keepAlive' class='page-content'></router-view>
+            </div>
+        </div>
+        <!-- <div class='page-footer'>
+            © 2016 SAGACLOUD. All rights reserved 京ICP备16059843号-1
+        </div>-->
+    </div>
+</template>
+<script>
+import { mapGetters, mapMutations } from 'vuex'
+import PageHeader from './PageHeader'
+import PageSidebar from './PageSidebar'
+import menus from '@/data/menus' 
+export default {
+    name: 'Main',
+    components: {
+        'page-header': PageHeader,
+        'page-sidebar': PageSidebar
+    },
+    props: [],
+    data() {
+        return {
+            isPath:  false
+        }
+    },
+    computed: {
+        ...mapGetters('layout', ['sidebarClosed', 'breadcrumb'])
+    },
+    methods: {
+        ...mapMutations('layout', ['setSidebarClosed','setSidebarSelected']),
+        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)
+            }
+        },
+        findPathInArray(arr, path) {
+            arr.forEach(ele => {
+                if(ele.path == path) {
+                    this.isPath = true
+                }
+                if(ele.children) {
+                   this.findPathInArray(ele.children, path)
+                }
+            })
+        }
+    },
+    watch: {
+        "$route": {
+            handler: function(route, oldVal){
+                let path = route.path
+                this.isPath = false;
+                this.findPathInArray(menus,path)
+                if(this.isPath) {
+                    this.setSidebarSelected(path)
+                }
+            },
+            // 深度观察监听
+            deep: true
+        }
+    },
+}
+</script>

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

@@ -0,0 +1,139 @@
+<template>
+    <div class='page-header'>
+        <div id='page-header-logo' class='page-logo'>
+            <a @click="toIndex">
+                <img src='@/assets/logo.png' alt='logo' class='logo-default'>
+            </a>
+            <div class='menu-toggler sidebar-toggler' @click.stop='toggleSidebar'>
+                <span>
+                    <i class='el-icon-fa-bars'></i>
+                </span>
+            </div>
+        </div>
+        <div id='page-header-data-menu' class='data-menu'>
+            <!-- <el-select v-model='selectedProjectId' placeholder='请选择' filterable>
+                <el-option-group v-for="group in projectGropList" :key="group.label" :label="group.label">
+                    <el-option v-for='item in group.options' :key='item.id' :label='item.name' :value='item.id'></el-option>
+                </el-option-group>
+            </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}} -->
+                    admin
+                    <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 frameworkApi from '@/api/framework'
+import { mapGetters, mapMutations } from 'vuex'
+import getFirstLetter from '@/utils/getFirstLetter'
+import lStorage from '@/utils/localStorage'
+export default {
+    name: 'PageHeader',
+    components: {},
+    props: [],
+    data() {
+        return {
+            selectedProjectId: null
+        }
+    },
+    created() {
+        // let cacheInfo = lStorage.get('historyInfo')?lStorage.get('historyInfo'):{}
+        // if(cacheInfo[this.userInfo.userName] && 
+        //     cacheInfo[this.userInfo.userName][0] && 
+        //     this.projects.some((item) => {return item.id == cacheInfo[this.userInfo.userName][0]})
+        // ){
+        //     this.selectedProjectId = cacheInfo[this.userInfo.userName][0]
+        // } else {
+        //     this.selectedProjectId = this.projectId
+        // }
+    },
+    mounted() {},
+    computed: {
+        ...mapGetters('layout', ['sidebarClosed', 'userInfo', 'projects', 'projectId']),
+        // projectGropList () {
+        //     const cacheInfo = lStorage.get('historyInfo')?lStorage.get('historyInfo'):{}
+        //     let options = [{
+        //         label: '最近使用',
+        //         options: []
+        //     }]
+        //     const projectHistory = cacheInfo[this.userInfo.userName] || []
+        //     options[0].options = projectHistory.map(projectId => {
+        //         let obj = this.projects.find(project => {
+        //             return project.id === projectId
+        //         })
+        //         return obj?obj:[]
+        //     })
+
+        //     let helpObj = {}
+        //     this.projects.forEach(p => {
+        //         const label = getFirstLetter((p.name || p.id).slice(0, 1))
+        //         if (helpObj[label]) {
+        //             helpObj[label].push(p)
+        //         } else {
+        //             helpObj[label] = [p]
+        //         }
+        //     })
+        //     const arr = Object.keys(helpObj).sort().map(label => {
+        //         return {
+        //             label: label,
+        //             options: helpObj[label]
+        //         }
+        //     })
+        //     return [...(options[0].options.length > 0 ? options : []), ...arr]
+        // }
+    },
+    watch: {
+        selectedProjectId(n, o) {
+            this.setprojectId(n)
+        }
+    },
+    methods: {
+        ...mapMutations('layout', ['setSidebarClosed', 'setprojectId']),
+        handleSelect() {},
+        toggleSidebar() {
+            this.setSidebarClosed(!this.sidebarClosed)
+        },
+        userMenuCommand(cmd) {
+            // console.log('userMenuCommand ', cmd)
+            if (cmd == 'logout') {
+                console.log("点击登出l")
+                frameworkApi.toLogout()
+            }
+        },
+        toIndex() {
+            this.$router.push('/')
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+.header-nav-Splitscreen>a{
+    display: flex;
+    width: 100%;
+    height: 100%;
+    position: relative;
+    padding: 16px 10px 10px;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    color: #79869a;
+    font-size: 24px;
+    text-decoration: none;
+    cursor: pointer;
+    i{transition:all 0ms ease 0ms}
+}
+.header-nav-Splitscreen>a:hover{
+    text-decoration: none;
+    color: #d3d8e2;
+    background-color: #3f4f62;
+}
+</style>

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

@@ -0,0 +1,91 @@
+<template>
+    <div id='page-sidebar-wrapper' class='page-sidebar-wrapper'>
+        <el-scrollbar tag="div" wrapClass="content-scrollbar">
+        <el-menu
+            :collapse='sidebarClosed'
+            :default-active='sidebarSelected'
+            @open='handleOpen'
+            @close='handleClose'
+            @select='menuSelect'
+            background-color='#364150'
+            text-color='#fff'
+            active-text-color='#ffd04b'
+            unique-opened
+            router
+            class='sidebar-menu'
+        >
+            <template v-for='menu in menus'>
+                <el-submenu v-if='menu.children' :index='menu.path' :key='menu.path' class='sidebar-menu-submenu'>
+                    <template slot='title'>
+                        <i v-if='menu.icon' :class='"iconfont "+ menu.icon'></i>
+                        <span>{{menu.name}}</span>
+                    </template>
+                    <template v-for='submenu in menu.children'>
+                        <el-submenu v-if='submenu.children' :index='submenu.path' :key='submenu.path' class='sidebar-menu-submenu'>
+                            <template slot='title'>
+                                <i v-if='submenu.icon' :class='"iconfont "+ submenu.icon'></i>
+                                <span>{{submenu.name}}</span>
+                            </template>
+                            <template v-for='threemenu in submenu.children'>
+                                <el-menu-item :index='threemenu.path' :key='threemenu.path' class='sidebar-menu-item'>
+                                    <i v-if='threemenu.icon' :class='"iconfont "+ threemenu.icon'></i>
+                                    <span slot='title'>{{threemenu.name}}</span>
+                                </el-menu-item>
+                            </template>
+                        </el-submenu>
+                        <el-menu-item v-else :index='submenu.path' :key='submenu.path' class='sidebar-menu-item'>
+                            <i v-if='submenu.icon' :class='"iconfont "+ 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' class='sidebar-menu-item' :disabled="menu.disabled">
+                    <i v-if='menu.icon' :class='"iconfont "+ menu.icon'></i>
+                    <span slot='title'>{{menu.name}}</span>
+                </el-menu-item>
+            </template>
+        </el-menu>
+        </el-scrollbar>
+    </div>
+</template>
+<script>
+import { mapGetters, mapMutations } from 'vuex'
+import frameworkApi from '@/api/framework'
+export default {
+    name: 'PageSidebar',
+    props: [],
+    data() {
+        return {}
+    },
+    computed: {
+        ...mapGetters('layout', ['sidebarClosed', 'sidebarSelected', 'permissions']),
+        menus() {
+            // console.log(this.permissions)
+            let menus = frameworkApi.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>
+

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

@@ -0,0 +1,153 @@
+import frameworkApi from '@/api/framework'
+import storage from '@/framework/utils/storage'
+import lStorage from '@/utils/localStorage'
+
+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: null, //{ username: 'admin' },
+      permissions: {
+        "system:role:delete": true,
+        "system:role:create": true,
+        "system:role:query": true,
+        "system:role:setOpts": true
+      },
+      projectId: '',
+      projects: [],
+      breadcrumb: [],
+      uploaderList: [], //当前上传文件列表
+      secret: "", //项目密码
+      userId: "", //用户id
+      rowEdit: false,//表格数据变化
+    },
+    getters: {
+      sidebarClosed: state => state.sidebarClosed,
+      secret: state => state.secret,
+      userId: state => state.userId,
+      sidebarSelected: state => {
+        // if (!state.pageSidebarSelected) {
+        //     let menu = storage.get(KEY_MENU_SELECTED)
+        //     if (menu) {
+        //         state.pageSidebarSelected = menu
+        //     }
+        // }
+        // return state.pageSidebarSelected
+        return state.sidebarSelected
+      },
+      userInfo: state => state.userInfo,
+      permissions: state => state.permissions,
+      projects: state => state.projects,
+      uploaderList: state => state.uploaderList,
+      projectId: state => {
+        if (!state.projectId) {
+          let pid = storage.get(KEY_PROJECT_SELECTED)
+          if (pid) {
+            state.projectId = pid
+          }
+        }
+        return state.projectId
+      },
+      breadcrumb: state => {
+        if (!state.breadcrumb) {
+          let arr = storage.get(KEY_PAGE_BRANDCRUMB)
+          if (arr) {
+            state.breadcrumb = arr
+          }
+        }
+        return state.breadcrumb
+      }
+    },
+    mutations: {
+      setRowEdit: (state, val) => (state.rowEdit = val),
+      setSidebarClosed: (state, val) => (state.sidebarClosed = val),
+      setSidebarSelected: (state, val) => {
+        state.sidebarSelected = val
+        storage.set(KEY_MENU_SELECTED, val)
+        lStorage.set('screen_data', {path: val, data: {}})
+      },
+      setprojectId: (state, val) => {
+        lStorage.remove('cacheInfo') //待删除(删除用户浏览器无用缓存)
+        let cacheInfo = lStorage.get('historyInfo') ? lStorage.get('historyInfo') : {}
+        state.projectId = val
+        localStorage.setItem('projectId', val)
+        if (cacheInfo[state.userInfo.userName]) {
+          // cacheInfo[state.userInfo.userName].projectId = val
+          cacheInfo[state.userInfo.userName] = [...new Set([val, ...cacheInfo[state.userInfo.userName]])].slice(0,3)
+          lStorage.set('historyInfo', cacheInfo)
+        } else {
+          cacheInfo[state.userInfo.userName] = [val]
+          lStorage.set('historyInfo', cacheInfo)
+        }
+        storage.set(KEY_PROJECT_SELECTED, val)
+        state.projects.map((item) => {
+          if (item.id == val) {
+            state.secret = item.pwd
+            localStorage.setItem('secret', item.pwd)
+          }
+        })
+      },
+      setUploaderList: (state, val) => {
+        state.uploaderList = val ? val : []
+      },
+
+    },
+  actions: {
+    setRowEdit(contentx, value) {
+      contentx.commit('setRowEdit', value)
+    },
+    loadUserInfo({state}) {
+      console.log(state)
+      return new Promise((resolve, reject) => {
+        frameworkApi.loadUserInfo().then(resp => {
+          console.log(resp)
+          if (resp.Result == 'success' && resp.UserId) {
+            state.userInfo = {userName: resp.Username, userId: resp.UserId}
+            state.userId = resp.UserId
+            storage.set('user_name', resp.Username)
+            storage.set('user_id', resp.UserId)
+            state.permissions = {}
+            if (resp.Permissions) {
+              resp.Permissions.forEach(p => (state.permissions[p] = true))
+            }
+            state.projects = []
+            if (resp.Projects) {
+              if (resp.Projects[0] && resp.Projects[0].ProjId) {
+                state.projectId = resp.Projects[0].ProjId
+                state.secret = resp.Projects[0].Secret ? resp.Projects[0].Secret : ""
+              }
+              resp.Projects.forEach(proj =>
+                state.projects.push({
+                  id: proj.ProjId,
+                  name: proj.ProjLocalName,
+                  pwd: proj.Secret ? proj.Secret : ""
+                })
+              )
+            }
+          } else {
+            state.userInfo = null
+
+          }
+          resolve(resp)
+        })
+      })
+    },
+    setBreadcrumb: {
+      root: true,
+      handler({state, commit}, val) {
+        let label = val[0].label;
+        if (label === "消息中心") {
+          commit("setSidebarSelected", "message"); // 当进入消息中心页面的时候不选中导航栏
+        }
+        state.breadcrumb = []
+        state.breadcrumb = val
+        storage.set(KEY_PAGE_BRANDCRUMB, val)
+      }
+    }
+  }
+}

+ 32 - 0
src/framework/plugins/components.js

@@ -0,0 +1,32 @@
+import store from '@/store'
+import TableTemplate from '@/framework/template/TablePageTemplate'
+//tools
+import basicutils from '@/framework/utils/basicutils'
+
+export default {
+    install: function(Vue) {
+        Vue.prototype.hasPermission = function(permission) {
+            return store.getters['layout/permissions'][permission]
+        }
+
+        Vue.component('table-page-template', TableTemplate)
+
+        Vue.prototype.indexInArray = basicutils.indexInArray
+        Vue.prototype.itemInArray = basicutils.itemInArray
+        Vue.prototype.deleteInArray = basicutils.deleteInArray
+
+        Vue.prototype.formatDate = function(date) {
+            return basicutils.formatDateByPattern(date, 'yyyy-MM-dd HH:mm:ss')
+        }
+
+        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')
+        })
+    }
+}

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

@@ -0,0 +1,424 @@
+@import './theme.scss';
+$pageHeaderHeight: 50px;
+$pageSidebarWidth: 235px;
+$pageSidebarClosedWidth: 64px;
+$pageSidebarItemHeight: 40px;
+$pageFooterHeight: 40px;
+html,
+body {
+    padding: 0 !important;
+    margin: 0 !important;
+    font-size: 14px;
+    width: 100%;
+    height: 100%;
+}
+
+#app {
+    width: 100%;
+    height: 100%;
+}
+/*---滚动条默认显示样式--*/
+::-webkit-scrollbar-thumb{
+    height:50px;
+    outline-offset:-2px;
+    outline:2px solid #dddee0;
+    border: 2px solid #dddee0;
+    background-color: #dddee0;
+    border-radius: 4px;
+}
+
+/*---鼠标点击滚动条显示样式--*/
+::-webkit-scrollbar-thumb:hover{
+    height:50px;
+}
+
+
+/*---滚动条大小--*/
+::-webkit-scrollbar{
+    width:8px;
+    height:8px;
+}
+
+/*---滚动框背景样式--*/
+::-webkit-scrollbar-track-piece{
+    
+}
+#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: auto;
+            flex-grow: 0;
+            flex-shrink: 0;
+        }
+        .page-content-wrapper {
+            flex-grow: 1;
+            flex-shrink: 1;
+        }
+    }
+    .page-footer {
+        order: 2;
+        flex-grow: 0;
+        flex-shrink: 0;
+        height: $pageFooterHeight;
+        line-height: $pageFooterHeight;
+        text-align: center;
+        color: #6d6d6d;
+    }
+}
+
+#page-main .page-header {
+    display: flex;
+    flex-direction: row;
+    /* 水平, 从左向右 */
+    justify-content: flex-start;
+    /* 垂直 左对齐 */
+    .page-logo {
+        flex-grow: 0;
+        flex-shrink: 0;
+        padding-left: 20px;
+        padding-right: 20px;
+        width: 195px;
+        display: inline-flex;
+        flex-direction: row;
+        justify-content: space-between;
+        >a {
+            display: inline-block;
+            .logo-default {
+                margin: 18px 0 0;
+                height: 18px;
+            }
+        }
+        .menu-toggler.sidebar-toggler {
+            margin-top: 10.5px;
+        }
+    }
+    .data-menu {
+        flex-grow: 0;
+        flex-shrink: 1;
+        width: 240px;
+        height: 100%;
+        line-height: $pageHeaderHeight;
+        .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;
+        .header-nav{
+            margin-right: 10px;
+            >li{
+                float: left;
+                width: 50px;
+                height: 50px;
+                padding: 0 4px;
+                box-sizing: border-box;
+            }
+        }
+        .user-menu-dropdown {
+            height: 100%;
+            line-height: $pageHeaderHeight;
+            font-size: 18px;
+            margin-left: 20px;
+        }
+    }
+}
+
+#page-main .page-container {
+    .page-sidebar-wrapper {
+        .el-menu {}
+    }
+}
+
+#page-main .page-container #page-sidebar-wrapper {
+    .sidebar-menu.el-menu {
+        border-right: none;
+        padding-bottom: 10px;
+        .el-submenu {
+            .el-menu-item {
+                height: $pageSidebarItemHeight;
+                line-height: $pageSidebarItemHeight;
+            }
+        }
+    }
+}
+
+
+/** sidebar 弹框 */
+
+.el-menu--vertical {
+    .el-menu.el-menu--popup.el-menu--popup-right-start {
+        .el-menu-item.sidebar-menu-item {
+            height: $pageSidebarItemHeight;
+            line-height: $pageSidebarItemHeight;
+        }
+    }
+}
+
+#page-main .page-container .page-content-wrapper {
+    margin: 0px 10px 10px 10px;
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-start;
+    overflow: hidden;
+    .page-bar {
+        flex-grow: 0;
+        flex-shrink: 0;
+        height: 30px;
+        padding: 0 10px;
+        margin-bottom: 10px;
+        border-bottom: 1px solid #333;
+        .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 {
+#page-main #page-container #page-content-wrapper .page-table-template {
+    // display: flex;
+    // flex-direction: column;
+    // justify-content: flex-start;
+    // align-items: stretch;
+    // flex-grow: 1;
+    // flex-shrink: 1;
+    height: 100%;
+    >.query-form {
+        flex-grow: 0;
+        flex-shrink: 0;
+    }
+    >.data-table {
+        // flex-grow: 1;
+        // flex-shrink: 1;
+        height: calc(100% - 54px);
+        // overflow-x: auto;
+        // overflow-y: auto;
+        background: #fff;
+        // margin-top: 10px;
+    }
+    >.data-table-pagination {
+        //     z-index: 99999;
+        margin-top: 10px;
+        //     flex-grow: 0;
+        //     flex-shrink: 0;
+        //     display: flex;
+        //     justify-content: flex-end;
+        text-align: right;
+        //     position: fixed;
+        //     bottom: 0;
+        //     background: #f2f2f2;
+        //     left: 242px;
+        //     right: 0;
+        //     padding-top: 10px;
+        //     padding-bottom: 10px;
+    }
+}
+
+#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 {}
+
+// #page-main #page-container #page-content-wrapper .page-content  .data-table {
+.data-table {
+    position: relative;
+    >.el-table {
+        position: absolute;
+        .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;
+                    }
+                    &.btn {
+                        padding: 2px 0;
+                    }
+                }
+            }
+        }
+    }
+}
+
+#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;
+            >a {
+                display: none;
+            }
+        }
+    }
+    .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;
+//         }
+//     }
+// }
+.content-scrollbar {
+    height: calc(100vh - 33px);
+    background: #364150;
+    .el-scrollbar__bar {
+        &.is-vertical {
+            display: none;
+        }
+    }
+}
+
+#page-main #page-container #page-content-wrapper .box {
+    display: flex;
+    flex-direction: column;
+    .el-tabs {
+        flex-grow: 1;
+        flex-shrink: 1;
+        display: flex;
+        flex-direction: column;
+        .el-tabs__header {
+            flex-grow: 0;
+            flex-shrink: 0;
+        }
+        .el-tabs__content {
+            flex-grow: 1;
+            flex-shrink: 1;
+            display: flex;
+            flex-direction: column;
+            .el-tab-pane {
+                height: 100%;
+                // flex-grow: 1;
+                // flex-shrink: 1;
+                display: flex;
+                flex-direction: column;
+                // // > div {
+                // //     flex-grow: 1;
+                // //     flex-shrink: 1;
+                // // }
+            }
+        }
+    }
+}

+ 106 - 0
src/framework/style/reset.scss

@@ -0,0 +1,106 @@
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, menu, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+main, menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: 100%;
+	font: inherit;
+	vertical-align: baseline;
+}
+
+html,
+body {
+    padding: 0 !important;
+    margin: 0 !important;
+    font-size: 14px;
+    color: #1F2429;
+    width: 100%;
+    height: 100%;
+    font-family: Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;
+    font-weight: 400;
+    -webkit-font-smoothing: antialiased;
+    -webkit-tap-highlight-color: transparent;
+}
+
+#app {
+    width: 100%;
+    height: 100%;
+}
+
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, main, menu, nav, section {
+	display: block;
+}
+/* HTML5 hidden-attribute fix for newer browsers */
+*[hidden] {
+    display: none;
+}
+body {
+	line-height: 1;
+}
+menu, ol, ul {
+	list-style: none;
+}
+blockquote, q {
+	quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+	content: '';
+	content: none;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+
+/*---滚动条默认显示样式--*/
+::-webkit-scrollbar-thumb{
+	height:50px;
+	outline-offset:-2px;
+	outline:2px solid #dddee0;
+	border: 2px solid #dddee0;
+	background-color: #dddee0;
+	border-radius: 4px;
+}
+
+/*---鼠标点击滚动条显示样式--*/
+::-webkit-scrollbar-thumb:hover{
+	height:50px;
+}
+
+
+/*---滚动条大小--*/
+::-webkit-scrollbar{
+	width:8px;
+	height:8px;
+}
+
+/*---滚动框背景样式--*/
+::-webkit-scrollbar-track-piece{
+	
+}
+
+
+/*---数字输入框去掉上下箭头--*/
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+  -webkit-appearance: none !important;
+  margin: 0; 
+}
+
+input[type="number"] {
+  -moz-appearance: textfield;
+}

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

@@ -0,0 +1,106 @@
+$colorWhite: #fff;
+
+$pageHeaderBackground: #2b3643;
+.body {
+    color: #333;
+}
+#app #page-main {
+    .page-header {
+        background: $pageHeaderBackground;
+
+        .page-logo {
+            .menu-toggler.sidebar-toggler {
+                color: $colorWhite;
+            }
+        }
+
+        .data-menu {
+            .data-menu-dropdown {
+                color: $colorWhite;
+            }
+            .el-select .el-input--suffix .el-input__inner {
+                background: #e3e4e6;
+            }
+        }
+        .user-menu {
+            .user-menu-dropdown {
+                color: $colorWhite;
+            }
+        }
+    }
+
+    #page-container {
+        background: #f2f2f2;
+        overflow: hidden;
+        #page-sidebar-wrapper {
+            color: azure;
+            background-color: #364150;
+            .sidebar-menu.el-menu {
+                background: #364150;
+
+                .sidebar-menu-submenu.el-submenu {
+                    &.is-active>.el-submenu__title {
+                        background: #36c6d3 !important;
+                        color: $colorWhite !important;
+                    }
+                    .el-submenu__title {
+                        height: 40px;
+                        line-height: 40px;
+                        &:hover {
+                            background: #000;
+                        }
+                    }
+                }
+                .sidebar-menu-item.el-menu-item {
+                    height: 40px;
+                    line-height: 40px;
+                    &:hover {
+                        background: #000;
+                    }
+                    &.is-active {
+                        background: #36c6d3 !important;
+                        color: $colorWhite !important;
+                    }
+                }
+            }
+        }
+        .page-content-wrapper {
+            .page-content {
+                .query-form {
+                    border: 1px solid #dfe6ec;
+                    background-color: $colorWhite;
+                }
+                .data-table {
+                    .el-table__header-wrapper {
+                        .el-table__header {
+                            thead tr th {
+                                background-color: #d9d9d9;
+                                color: #000;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    .page-footer {
+        background: #28303b;
+    }
+}
+
+.el-menu--vertical {
+    .el-menu.el-menu--popup.el-menu--popup-right-start {
+        .el-menu-item.sidebar-menu-item.is-active {
+            background: #36c6d3 !important;
+            color: $colorWhite !important;
+        }
+    }
+}
+
+.sidebar-menu-item.el-menu-item,
+.sidebar-menu-submenu.el-submenu .el-submenu__title {
+    i {
+        color: $colorWhite;
+    }
+}

+ 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>

+ 88 - 0
src/framework/utils/basicutils.js

@@ -0,0 +1,88 @@
+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)
+                }
+            }
+        }
+    }
+}

+ 33 - 0
src/framework/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)
+    }
+}

+ 32 - 0
src/main.js

@@ -0,0 +1,32 @@
+// 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 'element-ui/lib/theme-chalk/base.css';
+import CollapseTransition from 'element-ui/lib/transitions/collapse-transition';
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+Vue.use(ElementUI, { size: "small", zIndex: 1000 })
+
+Vue.component(CollapseTransition.name, CollapseTransition)
+import './framework/style/layout.scss'
+import './framework/style/reset.scss'
+
+import '@/assets/style/style.scss'
+import '@/assets/style/iconfont/iconfont.css'
+
+import componentsPlugin from '@/framework/plugins/components'
+Vue.use(componentsPlugin)
+
+Vue.config.productionTip = false
+Vue.config.devtools = true;
+/* eslint-disable no-new */
+new Vue({
+    el: '#app',
+    router,
+    store,
+    components: { App },
+    template: '<App/>'
+})

+ 18 - 0
src/router/index.js

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

+ 17 - 0
src/router/system.js

@@ -0,0 +1,17 @@
+import LayoutMain from '@/framework/layout/Main'
+import Index from '@/views/index'
+import auth from '@/views/system/auth'
+import noUser from '@/views/system/nouser'
+
+export default [
+    {
+        path: '/',
+        name: '',
+        component: LayoutMain,
+        children: [
+            { path: '', name: 'blank', component: Index },
+        ]
+    },
+    { path: '/auth', name: 'auth', component: auth },
+    { path: '/noUser', name: 'noUser', component: noUser },
+]

+ 38 - 0
src/store/index.js

@@ -0,0 +1,38 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import storage from '@/framework/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,
+  }
+})

+ 91 - 0
src/utils/authutils.js

@@ -0,0 +1,91 @@
+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
+    console.log('tologin ', `${ssoServer}/login?redirectUrl=${redirectUrl}/auth`)
+    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' || to.path == '/nouser') {
+            next()
+        // } else {
+        //     let userInfo = store.getters['layout/userInfo']
+        //     console.log("user info ", userInfo)
+        //     if (!userInfo) {
+        //         // 本地是未登录状态, 保存目标页面地址, 去登录
+        //         let lastRoute = { path: to.path, params: to.params, query: to.query }
+        //         store.commit('setLastRoute', lastRoute)
+        //         toLogin()
+        //     } else {
+        //         if (to.meta.breadcrumbs) {
+        //             store.dispatch('setBreadcrumb', to.meta.breadcrumbs)
+        //         }
+        //          next()
+        //     }
+        //     return true
+        // }
+    },
+    toNoUser(){
+        console.log(this)
+        this.$router.replace('/nouser')
+    },
+
+    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
+}

+ 2 - 0
src/utils/bus.js

@@ -0,0 +1,2 @@
+import Vue from 'vue'
+export default new Vue

File diff suppressed because it is too large
+ 381 - 0
src/utils/getFirstLetter.js


+ 107 - 0
src/utils/httputils.js

@@ -0,0 +1,107 @@
+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: 3000000, // 请求超时时间
+    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(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/localStorage.js

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

+ 64 - 0
src/utils/tools.js

@@ -0,0 +1,64 @@
+const tools = {}
+
+//存储cookie
+tools.setCookie = (name, value) => {
+    var Days = 30; /* 设置cookie保存时间 */
+    var exp = new Date();
+    exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
+    document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
+}
+
+
+//获取cookie
+tools.getCookie = name => {
+    var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
+    if (arr = document.cookie.match(reg)) {
+        return unescape(arr[2]);
+    } else {
+        /* 如果没有参数,那么就获取本域下的所有cookie */
+        return document.cookie;
+    }
+}
+
+tools.delObjKey = (obj, key) => {
+    if (obj.hasOwnProperty(key)) {
+        delete obj[key]
+    }
+}
+
+tools.deepCopy = (obj) => {
+    let temp = obj.constructor === Array ? [] : {}
+    for (let val in obj) {
+        temp[val] = typeof obj[val] == 'object' ? tools.deepCopy(obj[val]) : obj[val]
+    }
+    return temp
+}
+
+//判断watch监听的值是否相同
+tools.isSimilarly = (newValue, oldValue) => {
+    if(typeof newValue === typeof oldValue){
+        if(newValue === oldValue){
+            return true
+        } else {
+            let flag = false
+            try {
+                Object.keys(newValue).forEach((item, index, arr) => {
+                    if(oldValue.hasOwnProperty(item) && newValue[item] === oldValue[item]){
+                        if(index == arr.length-1){
+                            flag = true
+                        }
+                    } else {
+                        throw new Error()
+                    }
+                })
+            } catch (error) {
+                return false
+            }
+            return flag
+        }
+    } else {
+        return false
+    }
+}
+
+export default tools

+ 36 - 0
src/views/index/index.vue

@@ -0,0 +1,36 @@
+<template>
+    <div>
+      <das-board>
+        <template v-slot:plan>
+          <span>{{plan?plan:"yyyy-mm-dd"}}</span>
+        </template>
+        <template v-slot:finish>
+          <span>{{finish?finish:"yyyy-mm-dd"}}</span>
+        </template>
+        <template v-slot:onLine>
+          <span>{{onLine?onLine:"yyyy-mm-dd"}}</span>
+        </template>
+        <template v-slot:explain>
+          <span>{{explain?explain:"yyyy-mm-dd"}}</span>
+        </template>
+      </das-board>
+    </div>
+</template>
+
+<script>
+import dasBoard from "@/components/dasboard";
+export default {
+    components: {
+      dasBoard
+    },
+    data() {
+        return {
+          plan: "未定",
+          finish: "未定",
+          onLine: "未定",
+          explain: "中台工具"
+        }
+    },
+    mounted() {}
+}
+</script>

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

@@ -0,0 +1,39 @@
+<template>
+    <div>
+        <h4>请稍候...</h4>
+    </div>
+</template>
+<script>
+import { mapGetters, mapMutations } from 'vuex'
+import authutils from '@/utils/authutils'
+export default {
+    name: 'index',
+    props: [],
+    data() {
+        return {}
+    },
+    created() {
+        let token = this.$route.query.token
+        console.log(token)
+        this.$store.commit('setSsoToken', token)   
+        console.log(this.$store.state.ssoToken)         
+        this.$store.dispatch('layout/loadUserInfo').then(resp => {
+            console.log('store dispatch result ', resp)
+            // if (resp.result  == 'success') {
+            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({path: "/"})
+                }
+            }else{
+                this.$router.replace('/nouser')
+            }
+        })
+    },
+    mounted() {},
+    components: {}
+}
+</script>

+ 23 - 0
src/views/system/nouser/index.vue

@@ -0,0 +1,23 @@
+<template>
+    <div>
+        <h4>用户未授权,请联系管理员!</h4>
+    </div>
+</template>
+<script>
+export default {
+    name: 'index',
+    props: [],
+    data() {
+        return {}
+    },
+    computed: {},
+    methods: {},
+    created() {
+        console.log('--------------------- index created')
+    },
+    mounted() {},
+    components: {}
+}
+</script>
+<style lang='scss' scoped>
+</style>

+ 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>

+ 78 - 0
src/views/system/role/RoleEdit.vue

@@ -0,0 +1,78 @@
+<template>
+    <el-dialog title='创建角色' :visible.sync='dialogVisible'>
+        <el-form ref='form' :model='formValue' :rules='roleRules' label-width='100px'>
+            <el-form-item label='角色名' prop='name'>
+                <el-input v-model.trim='formValue.name'></el-input>
+            </el-form-item>
+            <el-form-item label='备注' prop='remark'>
+                <el-input v-model.trim='formValue.remark'></el-input>
+            </el-form-item>
+        </el-form>
+        <div class='btn-box'>
+            <el-button type='primary' size='mini' @click='save'>创建角色</el-button>
+        </div>
+    </el-dialog>
+</template>
+<script>
+import roleapi from '@/api/system/role'
+export default {
+    name: 'RoleEdit',
+    data() {
+        return {
+            title: '创建角色',
+            dialogVisible: false,
+            type: null,
+            formValue: { id: null, name: null, remark: null },
+            roleRules: {
+                name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }]
+            }
+        }
+    },
+    methods: {
+        show(row) {
+            console.log(row)
+            this.dialogVisible = true
+            if (row) {
+                this.type = 'update'
+                this.title = '编辑角色'
+                this.formValue.id = row.id
+                this.formValue.name = row.name
+                this.formValue.remark = row.remark
+            } else {
+                this.type = 'create'
+                this.title = '创建角色'
+                this.formValue.id = null
+                this.formValue.name = null
+                this.formValue.remark = null
+            }
+        },
+        save() {
+            this.$refs['form'].validate(valid => {
+                if (valid) {
+                    console.log(this.formValue)
+                    if (this.type == 'create') {
+                        roleapi.create(this.formValue).then(resp => {
+                            console.log('create role ', resp)
+                            this.$message.success(this.title + '成功')
+                            this.$emit('saved')
+                            this.dialogVisible = false
+                        })
+                    } else {
+                        roleapi.update(this.formValue).then(resp => {
+                            console.log('update role ', resp)
+                            this.$message.success(this.title + '成功')
+                            this.$emit('saved')
+                            this.dialogVisible = false
+                        })
+                    }
+                }
+            })
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+.btn-box {
+    text-align: center;
+}
+</style>

+ 92 - 0
src/views/system/role/RoleList.vue

@@ -0,0 +1,92 @@
+<template>
+    <table-page-template>
+        <el-form :inline='true' slot='form' size='mini'>
+            <el-form-item label='角色'>
+                <el-input class='input' v-model.trim='searchValue' placeholder='请输入角色名进行查询' clearable></el-input>
+            </el-form-item>
+            <el-form-item>
+                <el-button v-if='hasPermission("system:role:query")' type='primary' @click='query'>查询</el-button>
+                <el-button type='primary' @click='editRole(null)'>创建角色</el-button>
+            </el-form-item>
+        </el-form>
+        <el-table :data='roles' border stripe style='width: 100%' slot='table'>
+            <el-table-column prop='id' label='ID' width='50'></el-table-column>
+            <el-table-column prop='name' label='Name'></el-table-column>
+            <el-table-column prop='remark' label='备注'></el-table-column>
+            <el-table-column label='操作' width='250'>
+                <template slot-scope='scope'>
+                    <el-button @click='editRole(scope.row)' type='primary' size='small'>编辑</el-button>
+                    <el-button @click='editAuth(scope.row)' type='primary' size='small'>配置权限</el-button>
+                    <el-button @click='deleteRole(scope.row)' type='danger' size='small'>删除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div slot='dialog'>
+            <role-edit ref='roleEdit' @saved='query'></role-edit>
+            <role-manage ref='roleManage'></role-manage>
+        </div>
+    </table-page-template>
+</template>
+<script>
+import roleapi from '@/api/system/role'
+import RoleEdit from './RoleEdit'
+import RoleManage from './RoleManage'
+export default {
+    name: 'RoleList',
+    data() {
+        return {
+            roles: [],
+            searchValue: ''
+        }
+    },
+    methods: {
+        query() {
+            let criteria = {}
+            if (this.searchValue) {
+                criteria.name = { $like: this.searchValue + '%' }
+            }
+            roleapi.query({ criteria }).then(resp => {
+                this.roles = resp.content
+            })
+        },
+        editRole(row) {
+            this.$refs['roleEdit'].show(row)
+        },
+        editAuth(row) {
+            this.$refs['roleManage'].show(row.id)
+        },
+        deleteRole(row) {
+            let opt = {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }
+            this.$confirm('此操作将永久删除, 是否继续?', '提示', opt)
+                .then(() => {
+                    let params = { id: row.id }
+                    roleapi.delete(params).then(res => {
+                        if (res.result == 'success') {
+                            this.$message.success('删除成功')
+                            this.query()
+                        } else {
+                            this.$message.error('删除失败: ' + res.message)
+                        }
+                    })
+                })
+                .catch(() => {})
+        }
+    },
+    created() {
+        this.query()
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.query()
+        })
+    },
+    components: {
+        RoleEdit,
+        RoleManage
+    }
+}
+</script>

+ 188 - 0
src/views/system/role/RoleManage.vue

@@ -0,0 +1,188 @@
+<template>
+    <el-dialog title='权限管理' :visible.sync='dialogVisible' top='5vh' width='60%'>
+        <div class='box'>
+            <div class='custom-tree-container'>
+                <div class='block'>
+                    <el-tree
+                        ref='tree'
+                        :data='data'
+                        node-key='id'
+                        default-expand-all
+                        show-checkbox
+                        :props='treeProps'
+                        :expand-on-click-node='false'
+                        :default-checked-keys='checkedBoxArr'
+                        @check='getCurrentKey'
+                    >
+                        <span class='custom-tree-node' slot-scope='{ node, data }'>
+                            <span>{{ node.label }}</span>
+                            <span v-if='data.level == "second"'>
+                                <el-checkbox
+                                    v-for='(opt,index) in data.opts'
+                                    :key='index'
+                                    v-model='opt.checked'
+                                    @change='changeCheckBox(node, data)'
+                                >{{opt.name}}</el-checkbox>
+                            </span>
+                        </span>
+                    </el-tree>
+                </div>
+            </div>
+            <div class='btn-box'>
+                <el-button type='primary' @click='save'>保 存</el-button>
+            </div>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import roleapi from '@/api/system/role'
+import menus from '@/data/menus'
+import utils from './eleTreeUtils'
+export default {
+    name: 'ele-tree',
+    data() {
+        return {
+            roleId: null,
+            dialogVisible: false,
+            treeProps: {
+                label: 'name'
+            },
+            permissions: [],
+            data: [],
+            checkedBoxArr: [] //被选中的数组
+        }
+    },
+    methods: {
+        show(roleId) {
+            this.roleId = roleId
+            console.log('edit role id = ', roleId)
+            roleapi.getOperations(roleId).then(resp => {
+                console.log('role opts ', resp)
+                this.permissions = resp.data ? resp.data : []
+                this.init()
+            })
+            this.dialogVisible = true
+        },
+        changeCheckBox(node, data) {
+            utils.changeChecked(this.data)
+            let flag = true
+            if (data.opts) {
+                data.opts.forEach(opt => {
+                    if (!opt.checked) {
+                        flag = false
+                    }
+                })
+            }
+            if (flag) {
+                this.$refs['tree'].setChecked(data.id, true)
+            } else {
+                this.$refs['tree'].setChecked(data.id, false)
+            }
+        },
+        //tree发生变化
+        getCurrentKey(node, keys) {
+            let checkedId = node.id
+            //id-dict
+            let checkedObjIds = {}
+            keys.checkedKeys.forEach(id => {
+                checkedObjIds[id] = true
+            })
+            //选中的时候
+            keys.checkedNodes.forEach(node => {
+                if (node.opts) {
+                    node.opts.forEach(opt => {
+                        opt.checked = true
+                    })
+                }
+            })
+            //取消选中的时候
+            if (checkedObjIds[checkedId]) {
+            } else {
+                //点击第二级
+                if (node.opts) {
+                    node.opts.forEach(ele => {
+                        ele.checked = false
+                    })
+                }
+                //点击第一级
+                if (node.children) {
+                    node.children.forEach(second => {
+                        if (second.opts) {
+                            second.opts.forEach(opt => {
+                                opt.checked = false
+                            })
+                        }
+                    })
+                }
+            }
+        },
+        save() {
+            let arr = []
+            this.data.forEach(first => {
+                if (first.children) {
+                    first.children.forEach(second => {
+                        if (second.opts) {
+                            second.opts.forEach(opt => {
+                                if (opt.checked) {
+                                    arr.push(opt.permission)
+                                }
+                            })
+                        }
+                    })
+                }
+            })
+            roleapi.setOperations({ roleId: this.roleId, opts: arr }).then(resp => {
+                if (resp.result == 'success') {
+                    this.$message.success('保存成功')
+                    this.dialogVisible = false
+                } else {
+                    this.$message.error('保存失败')
+                }
+            })
+        },
+        isCheckBasic() {
+            utils.changeChecked(this.data)
+        },
+        init() {
+            let _menus = []
+            if (process.env.NODE_ENV != 'production') {
+                _menus = _menus.concat(menus.demoMenus)
+            }
+            _menus = _menus.concat(menus.menus)
+            let arr = utils.initCheckboxList(_menus, this.permissions)
+            //获取默认选中
+            this.checkedBoxArr = utils.getCheckedNodeArr(arr)
+            this.data = arr
+        }
+    },
+    created() {
+        this.init()
+    }
+}
+</script>
+
+<style>
+.custom-tree-node {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 14px;
+    padding-right: 8px;
+}
+.el-tree-node__content {
+    border-bottom: 1px solid #e6e6e6;
+}
+</style>
+<style scoped lang='scss'>
+.box {
+    max-height: 500px;
+    overflow: hidden;
+    .btn-box {
+        margin-top: 10px;
+        text-align: center;
+    }
+}
+</style>
+

+ 132 - 0
src/views/system/role/eleTreeUtils.js

@@ -0,0 +1,132 @@
+//初始化checkBox--list
+const utils = {
+    initCheckboxList(menus, permissions) {
+        var permissObj = {}
+        permissions.forEach(permission => {
+            permissObj[permission] = true
+        })
+        let arr = []
+        var id = 1;
+        menus.forEach(first => {
+            let firstObj = {id: id++, name: first.name, level: 'first' }
+            if (first.children) {
+                firstObj.children = []
+                first.children.forEach(second => {
+                    let secondObj = { id: id++, name: second.name, level: 'second' }
+                    if (second.opts) {
+                        secondObj.opts = []
+                        second.opts.forEach(third => {
+                            let flag = this.isChecked(third, permissObj)
+                            let thirdObj = {}
+                            if (flag) {
+                                thirdObj = { id: id++, name: third.name, checked: true, permission: third.permission, basic: third.basic }
+                            } else {
+                                thirdObj = { id: id++, name: third.name, checked: false, permission: third.permission, basic: third.basic }
+                            }
+                            secondObj.opts.push(thirdObj)
+                        })
+                    }
+                    firstObj.children.push(secondObj)
+                })
+            }
+            arr.push(firstObj)
+        })
+        return this.isBasic(arr, permissObj)
+    },
+    // 判断当前是否在权限里
+    isChecked(opt, permissObj) {
+        if (permissObj[opt.permission]) {
+            return true
+        } else {
+            return false
+        }
+    },
+    //判断是否为basic,[basic同级 假如有checked的则basic为true]
+    isBasic(arr, permissObj) {
+        arr.forEach(first => {
+            if (first.children) {
+                first.children.forEach(second => {
+                    if (second.children) {
+                        second.children.forEach(opts => {
+                            if (opts.basic) {
+                                // 1。是basic
+                                if (permissObj[opts.value]) {
+                                    //2.有权限
+                                } else {
+                                    let flag = this.isBasicOther(second.children)
+                                    if (flag) {
+                                        opts.checked = true
+                                    }
+                                }
+                            }
+                        })
+                    }
+                })
+            }
+        })
+        return arr
+    },
+    //判断和basic同级的是否有被选中的
+    isBasicOther(arr) {
+        let flag = false
+        arr.forEach(opt => {
+            if (!opt.basic) {
+                if (opt.checked) {
+                    flag = true
+                }
+            }
+        })
+        return flag
+    },
+    /********************************************************************************** */
+    //当点击的时候,判断时候为basic,如果不是,查看basic是否选中
+    changeChecked(data) {
+        data.forEach(first => {
+            if (first.children) {
+                first.children.forEach(second => {
+                    if (second.opts) {
+                        second.opts.forEach(opt => {
+                            // console.log(opt)
+                            if (opt.basic) {
+                                //基本权限
+                            } else {
+                                if (opt.checked) {
+                                    this.checkedBasic(second.opts)
+                                }
+                            }
+                        })
+                    }
+                })
+            }
+        })
+    },
+    checkedBasic(arr) {
+        arr.forEach(item => {
+            if(item.basic) {
+                item.checked  = true
+            }
+        })
+    },
+    getCheckedNodeArr(arr) {
+        let checkedIdArr = [];
+        arr.forEach(first => {
+            if(first.children) {
+                first.children.forEach(second => {
+                    if(second.opts) {
+                        let flag = true
+                        second.opts.forEach(opt => {
+                            if(!opt.checked){
+                                flag = false
+                            }
+                        })
+                        if(flag) {
+                            checkedIdArr.push(second.id)
+                        }
+                    }
+                })
+            }
+        })
+        return checkedIdArr
+    }
+}
+export default utils

+ 100 - 0
src/views/system/user/UserAdd.vue

@@ -0,0 +1,100 @@
+<template>
+    <el-dialog :title='title' :visible.sync='dialogFormVisible' @close='close'>
+        <el-form :model='form' :rules='newUserRules' ref='loginForm'>
+            <el-form-item label='用户名' :label-width='formLabelWidth'>
+                <el-input v-model='form.name' auto-complete='off'></el-input>
+            </el-form-item>
+            <el-form-item label='备注' :label-width='formLabelWidth'>
+                <el-input v-model='form.remark' auto-complete='off'></el-input>
+            </el-form-item>
+            <el-form-item label='角色' :label-width='formLabelWidth'>
+                <el-checkbox-group v-model='form.roleIds'>
+                    <el-checkbox v-for='role in roles' :label='role.id' :key='role.id'>{{role.name}}</el-checkbox>
+                </el-checkbox-group>
+            </el-form-item>
+        </el-form>
+        <div slot='footer' class='dialog-footer'>
+            <el-button @click='close'>取消</el-button>
+            <el-button type='primary' @click='save()'>保存</el-button>
+        </div>
+    </el-dialog>
+</template>
+<script>
+import userapi from '@/api/system/user'
+import roleapi from '@/api/system/role'
+export default {
+    name: 'UserAdd',
+    data() {
+        return {
+            dialogFormVisible: false,
+            formLabelWidth: '120px',
+            roles: null,
+            title: null,
+            form: {
+                id: null,
+                name: null,
+                remark: null,
+                roleIds: []
+            },
+            newUserRules: {
+                username: [{ required: true, trigger: 'blur', validator: this.validateUsername }]
+            }
+        }
+    },
+    methods: {
+        open(data) {
+            let vm = this
+            vm.dialogFormVisible = true
+            if (!vm.roles) {
+                roleapi.query({ criteria: {} }).then(resp => (vm.roles = resp.content))
+            }
+            if (data) {
+                this.title = '编辑用户'
+                vm.form.id = data.id
+                vm.form.name = data.name
+                vm.form.remark = data.remark
+                vm.form.roleIds = []
+                if (data.roles) {
+                    data.roles.forEach(item => vm.form.roleIds.push(item.id))
+                }
+            } else {
+                this.title = '创建用户'
+            }
+        },
+        save() {
+            let vm = this
+            let url
+            if (vm.form.id) {
+                userapi.update(vm.form).then(this.saveResult)
+            } else {
+                userapi.create(vm.form).then(this.saveResult)
+            }
+        },
+        saveResult(resp) {
+            if (resp.result == 'success') {
+                this.$message.success(this.title + '成功')
+                this.dialogFormVisible = false
+                this.$emit('confirm')
+            } else {
+                this.$message.error(this.title + '失败')
+            }
+        },
+        validateUsername() {
+            return true
+        },
+        close() {
+            let vm = this
+            vm.dialogFormVisible = false
+            if (vm.form.id) {
+                vm.form.id = null
+                vm.form.name = null
+                vm.form.remark = null
+                vm.form.roleIds = []
+            }
+            vm.$emit('cancel')
+        }
+    }
+}
+</script>
+<style lang="css" scoped>
+</style>

+ 87 - 0
src/views/system/user/UserList.vue

@@ -0,0 +1,87 @@
+<template>
+    <table-page-template>
+        <el-form :inline='true' :model='queryData' slot='form' size='mini'>
+            <el-form-item label='用户'>
+                <el-input v-model.trim='queryData.username' placeholder='请输入用户名进行查询' clearable></el-input>
+            </el-form-item>
+            <el-form-item>
+                <el-button type='primary' @click='query'>查询</el-button>
+                <el-button v-if='hasPermission("system:user:create")' type='primary' @click='openAdd(null)'>创建用户</el-button>
+            </el-form-item>
+        </el-form>
+        <el-table :data='pageData.users' border stripe slot='table'>
+            <el-table-column prop='id' label='ID'></el-table-column>
+            <el-table-column prop='name' label='Name'></el-table-column>
+            <el-table-column prop='remark' label='Remark'></el-table-column>
+            <el-table-column prop='status' label='Status'></el-table-column>
+            <el-table-column label='操作' width='150'>
+                <template slot-scope='{row}'>
+                    <el-button type='primary' size='mini' @click='openAdd(row)'>编辑</el-button>
+                    <el-button type='danger' size='mini' @click='deleteBtnClicked(row)'>删除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <base-pagination :total='pageData.total' @pageChanged='pageChanged' slot='pagination'></base-pagination>
+        <user-add ref='addUser' @confirm='getData' slot='dialog'></user-add>
+    </table-page-template>
+</template>
+<script>
+import userapi from '@/api/system/user'
+import UserAdd from './UserAdd'
+export default {
+    name: 'UserList',
+    data() {
+        return {
+            queryData: {
+                username: null
+            },
+            pageData: {
+                page: 1,
+                size: 50,
+                total: 0,
+                users: []
+            }
+        }
+    },
+    methods: {
+        openAdd(user) {
+            this.$refs.addUser.open(user)
+        },
+        pageChanged(page, size) {
+            this.pageData.page = page
+            this.pageData.size = size
+            this.getData(page, size)
+        },
+        query() {
+            this.pageData.page = 1
+            this.pageData.size = 10
+            this.getData()
+        },
+        async getData() {
+            let vm = this
+            let username = this.queryData.username
+            let criteria = {}
+            if (username) {
+                criteria.name = username
+            }
+
+            let request = { page: vm.pageData.page, size: vm.pageData.size, criteria: criteria }
+            userapi.query(request).then(resp => {
+                vm.pageData.total = resp.count
+                vm.pageData.users = resp.content
+            })
+        }
+    },
+    created() {
+        this.query()
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.query()
+        })
+    },
+    components: {
+        'user-add': UserAdd
+    }
+}
+</script>

+ 0 - 0
static/.gitkeep


BIN
static/favicon.ico


+ 18 - 0
tsconfig.json

@@ -0,0 +1,18 @@
+{
+  "compilerOptions": {
+    //语法编译为es5语法
+    "target": "es5",
+    //模块采用import语法且不作转换,转换工作交给gulp任务中的webpack2
+    "module": "es2015",
+    //用NPM模式加载
+    "moduleResolution": "node",
+    //对于没有Export的库进行兼容
+    "allowSyntheticDefaultImports": true,
+    //开启修饰器模式
+    "experimentalDecorators": true,
+    //识别基本的系统类库
+    "lib": [ "dom", "es5", "es2015", "es2015.promise" ],
+    "strict": true
+  },
+  "include": [ "./src/**/*" ]
+}