刘秋岐 3 years ago
commit
75bc250e52
90 changed files with 5545 additions and 0 deletions
  1. 6 0
      .babelrc
  2. 3 0
      .gitignore
  3. 57 0
      README.md
  4. 40 0
      build/build.js
  5. 45 0
      build/check-versions.js
  6. 9 0
      build/dev-client.js
  7. 84 0
      build/dev-server.js
  8. 64 0
      build/utils.js
  9. 1 0
      build/vendor-manifest.json
  10. 17 0
      build/vue-loader.conf.js
  11. 70 0
      build/webpack.base.conf.js
  12. 35 0
      build/webpack.dev.conf.js
  13. 29 0
      build/webpack.dll.conf.js
  14. 102 0
      build/webpack.prod.conf.js
  15. 6 0
      config/dev.env.js
  16. 52 0
      config/index.js
  17. 3 0
      config/prod.env.js
  18. 12 0
      index.html
  19. 71 0
      package.json
  20. 172 0
      service/api/userApi.js
  21. 14 0
      service/app.js
  22. BIN
      service/db/DB.db
  23. 9 0
      service/db/sqlMap.js
  24. 9 0
      src/App.vue
  25. BIN
      src/assets/logo.png
  26. 114 0
      src/components/common/Header.vue
  27. 19 0
      src/components/common/Home.vue
  28. 87 0
      src/components/common/Sidebar.vue
  29. 33 0
      src/components/entities/DailyForecastEntity.js
  30. 23 0
      src/components/models/CompositeDashboardModel.js
  31. 21 0
      src/components/models/CurrentWeatherModel.js
  32. 30 0
      src/components/models/DayForecastModel.js
  33. 17 0
      src/components/models/HourForecastModel.js
  34. 12 0
      src/components/models/Observable.js
  35. 49 0
      src/components/page/AppModel.js
  36. 20 0
      src/components/page/Card.vue
  37. 28 0
      src/components/page/CenteredFlexView.vue
  38. 237 0
      src/components/page/ConfigurationScreen.vue
  39. 99 0
      src/components/page/CurrentWeatherConditionCard.vue
  40. 46 0
      src/components/page/DayWeatherForecast.vue
  41. 124 0
      src/components/page/Identify.vue
  42. 178 0
      src/components/page/Login.vue
  43. 16 0
      src/components/page/MeasureDependentUnitsCorrespondent.js
  44. 14 0
      src/components/page/MeasureIndependentUnitsCorrespondent.js
  45. 99 0
      src/components/page/ModifyPassword.vue
  46. 126 0
      src/components/page/ModifyUser.vue
  47. 12 0
      src/components/page/Observable.js
  48. 46 0
      src/components/page/OpenWeatherApiAdapter.js
  49. 80 0
      src/components/page/Readme.vue
  50. 186 0
      src/components/page/Register.vue
  51. 78 0
      src/components/page/RegisterSuccess.vue
  52. 51 0
      src/components/page/Success.vue
  53. 51 0
      src/components/page/TimeScaleWeatherConditionView.vue
  54. 68 0
      src/components/page/ToggleView.vue
  55. 245 0
      src/components/page/Weather.vue
  56. 36 0
      src/components/page/WeatherConditionArt.vue
  57. 42 0
      src/components/page/WeatherConfiguration.js
  58. 9 0
      src/components/page/WeatherInfoEnum.js
  59. 17 0
      src/main.js
  60. 56 0
      src/router/index.js
  61. 6 0
      src/utils/utils.js
  62. 0 0
      static/.gitkeep
  63. 77 0
      static/css/color-dark.css
  64. 167 0
      static/css/datasource.css
  65. 113 0
      static/css/main.css
  66. 27 0
      static/css/theme-green/color-green.css
  67. BIN
      static/css/theme-green/fonts/element-icons.ttf
  68. BIN
      static/css/theme-green/fonts/element-icons.woff
  69. 1 0
      static/css/theme-green/index.css
  70. 162 0
      static/data.json
  71. 71 0
      static/datasource.json
  72. BIN
      static/fonts/AvenirNext-DemiBold.ttf
  73. BIN
      static/fonts/AvenirNext-Medium.ttf
  74. BIN
      static/fonts/AvenirNext-Regular.ttf
  75. BIN
      static/img/bg.png
  76. 1288 0
      static/img/bg.svg
  77. 58 0
      static/img/clear.svg
  78. 14 0
      static/img/clear_night.svg
  79. 6 0
      static/img/clouds.svg
  80. 30 0
      static/img/few_clouds.svg
  81. 2 0
      static/img/few_clouds_night.svg
  82. 10 0
      static/img/home.svg
  83. BIN
      static/img/img.jpg
  84. BIN
      static/img/logo.png
  85. 175 0
      static/img/logo.svg
  86. 2 0
      static/img/rain.svg
  87. 2 0
      static/img/refresh.svg
  88. 6 0
      static/img/send.svg
  89. 6 0
      static/js/vendor.dll.js
  90. 43 0
      static/vuetable.json

+ 6 - 0
.babelrc

@@ -0,0 +1,6 @@
+{
+  "presets": [
+    ["env", { "modules": false }],
+    "stage-3"
+  ]
+}

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+node_modules
+dist
+package-lock.json

+ 57 - 0
README.md

@@ -0,0 +1,57 @@
+# The Weather Helper #
+The Weather Helper is built with Vue.js + Element UI + Express + axios + SQLite3.
+
+There is a test account for you:
+
+​	Username: test
+
+​	Password: 123456
+
+
+
+## Screenshots
+
+![屏幕快照 2020-04-30 19.12.48.png](https://i.loli.net/2020/05/01/YQZd4jTmyNIMacq.png)
+
+![屏幕快照 2020-04-30 19.13.07.png](https://i.loli.net/2020/05/01/xz9AyY1kGKc4PiT.png)
+
+![屏幕快照 2020-04-30 19.13.58.png](https://i.loli.net/2020/05/01/yZ5DbRJXuASUokV.png)
+
+![屏幕快照 2020-04-30 19.13.33.png](https://i.loli.net/2020/05/01/ClShbDwUKEQMWyV.png)
+
+![屏幕快照 2020-04-30 19.14.39.png](https://i.loli.net/2020/05/01/fmjWLKz8Jer43Ct.png)
+
+![屏幕快照 2020-04-30 19.21.31.png](https://i.loli.net/2020/05/01/t83b4F2XzR6MgHr.png)
+
+![屏幕快照 2020-04-30 19.21.54.png](https://i.loli.net/2020/05/01/n3ke9sq1RDLY8Kx.png)
+
+
+
+## How to run it
+
+#### Step 1 - Install module dependencies
+
+	git clone https://github.com/Produce-404/Weather-Helper.git  //Download
+	cd Weather-Helper   // Enter main folder
+	npm install         // Install dependencies
+
+
+
+#### Step 2 - Open website (front-end)
+
+	// Open Website
+	npm run dev
+
+
+
+#### Step 3 - Run Server (back-end)
+
+	// run server
+	cd service
+	node app 
+
+
+
+#### Step 4 - Build
+
+	npm run build

+ 40 - 0
build/build.js

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

+ 45 - 0
build/check-versions.js

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

+ 9 - 0
build/dev-client.js

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

+ 84 - 0
build/dev-server.js

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

+ 64 - 0
build/utils.js

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

File diff suppressed because it is too large
+ 1 - 0
build/vendor-manifest.json


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

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

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

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

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

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

+ 29 - 0
build/webpack.dll.conf.js

@@ -0,0 +1,29 @@
+const path = require('path');
+const webpack = require('webpack');
+
+module.exports = {
+  entry: {
+    vendor: ['vue/dist/vue.common.js','vue-router', 'babel-polyfill','axios']
+  },
+  output: {
+    path: path.join(__dirname, '../static/js'),
+    filename: '[name].dll.js',
+    library: '[name]_library'
+  },
+  plugins: [
+    new webpack.DllPlugin({
+      path: path.join(__dirname, '.', '[name]-manifest.json'),
+      name: '[name]_library'
+    }),
+    new webpack.optimize.UglifyJsPlugin({
+      compress: {
+        warnings: false
+      }
+    }),
+    new webpack.DefinePlugin({
+      'process.env': {
+        NODE_ENV: '"production"'
+      }
+    })
+  ]
+};

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

@@ -0,0 +1,102 @@
+var path = require('path')
+var utils = require('./utils')
+var webpack = require('webpack')
+var config = require('../config')
+var merge = require('webpack-merge')
+var baseWebpackConfig = require('./webpack.base.conf')
+var HtmlWebpackPlugin = require('html-webpack-plugin')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+var env = config.build.env
+
+var webpackConfig = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.build.productionSourceMap,
+      extract: true
+    })
+  },
+  devtool: config.build.productionSourceMap ? '#source-map' : false,
+  output: {
+    path: config.build.assetsRoot,
+    filename: utils.assetsPath('js/[name].[chunkhash].js'),
+    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
+  },
+  plugins: [
+    // http://vuejs.github.io/vue-loader/en/workflow/production.html
+    new webpack.DefinePlugin({
+      'process.env': env
+    }),
+    new webpack.optimize.UglifyJsPlugin({
+      compress: {
+        warnings: false
+      },
+      sourceMap: true
+    }),
+    // extract css into its own file
+    new ExtractTextPlugin({
+      filename: utils.assetsPath('css/[name].[contenthash].css')
+    }),
+    // 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'
+    }),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+      minChunks: function (module, count) {
+        // any required modules inside node_modules are extracted to vendor
+        return (
+          module.resource &&
+          /\.js$/.test(module.resource) &&
+          module.resource.indexOf(
+            path.join(__dirname, '../node_modules')
+          ) === 0
+        )
+      }
+    }),
+    // extract webpack runtime and module manifest to its own file in order to
+    // prevent vendor hash from being updated whenever app bundle is updated
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'manifest',
+      chunks: ['vendor']
+    })
+  ]
+})
+
+if (config.build.productionGzip) {
+  var CompressionWebpackPlugin = require('compression-webpack-plugin')
+
+  webpackConfig.plugins.push(
+    new CompressionWebpackPlugin({
+      asset: '[path].gz[query]',
+      algorithm: 'gzip',
+      test: new RegExp(
+        '\\.(' +
+        config.build.productionGzipExtensions.join('|') +
+        ')$'
+      ),
+      threshold: 10240,
+      minRatio: 0.8
+    })
+  )
+}
+
+if (config.build.bundleAnalyzerReport) {
+  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
+}
+
+module.exports = webpackConfig

+ 6 - 0
config/dev.env.js

@@ -0,0 +1,6 @@
+var merge = require('webpack-merge')
+var prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+  NODE_ENV: '"development"'
+})

+ 52 - 0
config/index.js

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

+ 3 - 0
config/prod.env.js

@@ -0,0 +1,3 @@
+module.exports = {
+  NODE_ENV: '"production"'
+}

+ 12 - 0
index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>The Weather Helper</title>
+    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
+</head>
+<body>
+<div id="app"></div>
+
+</body>
+</html>

+ 71 - 0
package.json

@@ -0,0 +1,71 @@
+{
+  "name": "The-Weather-Helper",
+  "version": "2.1.0",
+  "description": "The Weather Helper built with Vue/Express/ElementUI/SQLite",
+  "author": "XiaoyueXiao and MaoyunChen",
+  "private": true,
+  "scripts": {
+    "dev": "node build/dev-server.js",
+    "build": "node build/build.js",
+    "build:dll": "webpack --config build/webpack.dll.conf.js"
+  },
+  "dependencies": {
+    "axios": "^0.15.3",
+    "babel-polyfill": "^6.23.0",
+    "body-parser": "^1.18.2",
+    "element-ui": "1.3.1",
+    "mysql": "^2.15.0",
+    "sqlite3": "^4.2.0",
+    "vue": "^2.3.2",
+    "vue-core-image-upload": "2.1.11",
+    "vue-datasource": "1.0.9",
+    "vue-quill-editor": "2.1.6",
+    "vue-resource": "^1.5.1",
+    "vue-router": "^2.3.1",
+    "vue-schart": "^0.1.2",
+    "vue-simplemde": "0.3.8"
+  },
+  "devDependencies": {
+    "autoprefixer": "^6.7.2",
+    "babel-core": "^6.22.1",
+    "babel-loader": "^6.2.10",
+    "babel-plugin-transform-runtime": "^6.22.0",
+    "babel-preset-env": "^1.7.0",
+    "babel-preset-es2015": "^6.22.0",
+    "babel-preset-stage-2": "^6.22.0",
+    "babel-register": "^6.22.0",
+    "chalk": "^1.1.3",
+    "connect-history-api-fallback": "^1.3.0",
+    "css-loader": "^0.28.0",
+    "eventsource-polyfill": "^0.9.6",
+    "express": "^4.14.1",
+    "extract-text-webpack-plugin": "^2.0.0",
+    "file-loader": "^0.11.1",
+    "friendly-errors-webpack-plugin": "^1.1.3",
+    "function-bind": "^1.1.0",
+    "html-webpack-plugin": "^2.28.0",
+    "http-proxy-middleware": "^0.17.3",
+    "opn": "^4.0.2",
+    "ora": "^1.2.0",
+    "semver": "^5.3.0",
+    "shelljs": "^0.7.6",
+    "url-loader": "^0.5.8",
+    "vue-loader": "^11.3.4",
+    "vue-style-loader": "^2.0.5",
+    "vue-template-compiler": "^2.2.6",
+    "webpack": "^2.3.3",
+    "webpack-bundle-analyzer": "^2.2.1",
+    "webpack-dev-middleware": "^1.10.0",
+    "webpack-hot-middleware": "^2.18.0",
+    "webpack-merge": "^4.1.0"
+  },
+  "engines": {
+    "node": ">= 4.0.0",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ]
+}

+ 172 - 0
service/api/userApi.js

@@ -0,0 +1,172 @@
+var express = require('express');
+var router = express.Router();
+var $sql = require('../db/sqlMap');
+const sqlite3 = require('sqlite3').verbose();
+
+
+
+var jsonWrite = function(res, ret) {
+    if(typeof ret === 'undefined') {
+        res.send('err');
+    } else {
+        console.log(ret);
+        res.send(ret);
+    }
+}
+
+var dateStr = function(str) {
+    return new Date(str.slice(0,7));
+}
+
+// add user
+router.post('/addUser', (req, res) => {
+    let db = new sqlite3.Database('./db/DB.db', (err) => {
+    if (err) {
+        console.error(err.message);
+    }
+        console.log('Connected to the database.');
+    });
+    
+    var sql = $sql.user.add;
+    var params = req.body;
+    console.log(params);
+    sql=sql+'(\''+params.name+'\',\''+params.account+'\',\''+params.pass+'\',\''+params.checkPass+'\',\''+String(params.email)+'\',\''+params.phone+'\',\''+params.card+'\',\''+params.birth+'\',\''+params.sex+'\')';
+    
+    db.get(sql, function(err, result) {
+        if (err) {
+            console.log(err);
+        }
+        else{
+            res.send('ok');
+        }
+    })
+    db.close();
+});
+
+
+//search user
+router.post('/login', (req, res) => {
+    let db = new sqlite3.Database('./db/DB.db', (err) => {
+    if (err) {
+        console.error(err.message);
+    }
+        console.log('Connected to the database.');
+    });
+    var sql_name = $sql.user.select_name;
+    var params = req.body;
+    var keywords = JSON.parse(Object.keys(params));
+    console.log(keywords);
+    if (keywords.name) {
+        sql_name += "where username ='"+ keywords.name +"'";
+    }
+    db.get(sql_name, function(err, result) {
+        if (err) {
+            console.log(err);
+        }
+        if (result === undefined) {
+            res.send('-1')   //cannot search username,return -1
+        } else {
+            if(result.password === keywords.password) {
+                jsonWrite(res, result);
+            } else {
+                res.send('0')  //username
+            }
+        }
+    })
+    db.close();
+});
+
+
+
+//get user information
+router.get('/getUser', (req, res) => {
+    let db = new sqlite3.Database('./db/DB.db', (err) => {
+    if (err) {
+        console.error(err.message);
+    }
+        console.log('Connected to the database.');
+    });
+    
+    var sql_name = $sql.user.select_name;
+    var params = req.body;
+    console.log(params);
+    if (params.name) {
+        sql_name += "where username ='"+ params.name +"'";
+    }
+    db.get(sql_name, function(err, result) {
+        if (err) {
+            console.log(err);
+        }
+        // console.log(result);
+        if (result[0] === undefined) {
+            res.send('-1')   //cannot search username,return -1
+        } else {
+            jsonWrite(res, result);
+        }
+    })
+    db.close();
+});
+
+//update user information
+router.post('/updateUser', (req, res) => {
+    let db = new sqlite3.Database('./db/DB.db', (err) => {
+    if (err) {
+        console.error(err.message);
+    }
+        console.log('Connected to the database.');
+    });    
+    var sql_update = $sql.user.update_user;
+    var params = req.body;
+    console.log(params);
+    if (params.id) {
+        sql_update  += " email = '" + params.email +
+                        "',phone = '" + params.phone +
+                        "',card = '" + params.card +
+                        "',birth = '" + params.birth +
+                        "',sex = '" + params.sex +
+                        "' where username ='"+ params.id + "'";
+    }
+    console.log(sql_update);
+    db.get(sql_update,function(err, result) {
+        if (err) {
+            console.log(err);
+        }
+        else {
+            res.send('ok'); 
+        }
+    })
+    db.close();
+});
+
+
+//update password
+router.post('/modifyPassword', (req, res) => {
+    let db = new sqlite3.Database('./db/DB.db', (err) => {
+    if (err) {
+        console.error(err.message);
+    }
+        console.log('Connected to the database.');
+    });
+    
+    var sql_modify = $sql.user.update_user;
+    var params = req.body;
+    console.log(params);
+    if (params.id) {
+        sql_modify +=  " password = '" + params.pass +
+                        "',repeatPass = '" + params.checkPass +
+                        "' where username ='"+ params.id + "'";
+    }
+    db.get(sql_modify, function(err, result) {
+        if (err) {
+            console.log(err);
+        }
+        // console.log(result);
+        else {
+            res.send('ok'); 
+        }
+    })
+    db.close();
+});
+
+
+module.exports = router;

+ 14 - 0
service/app.js

@@ -0,0 +1,14 @@
+const userApi = require('./api/userApi');
+const fs = require('fs');
+const path = require('path');
+const bodyParser = require('body-parser');
+const express = require('express');
+const app = express();
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded())
+
+app.use('/api/user', userApi);
+
+app.listen(3000);
+console.log('success listen at port: 3000')

BIN
service/db/DB.db


+ 9 - 0
service/db/sqlMap.js

@@ -0,0 +1,9 @@
+var sqlMap = {
+    user: {
+        add: 'insert into user (username, account, password, repeatPass, email, phone, card, birth, sex) values ',
+        select_name: 'select * from user ', 
+        update_user: 'update user set'
+    }
+}
+
+module.exports = sqlMap;

+ 9 - 0
src/App.vue

@@ -0,0 +1,9 @@
+<template>
+    <div id="app">
+        <router-view></router-view>
+    </div>
+</template>
+<style>
+    @import "../static/css/main.css";
+    @import "../static/css/color-dark.css";    
+</style>

BIN
src/assets/logo.png


+ 114 - 0
src/components/common/Header.vue

@@ -0,0 +1,114 @@
+<template>
+    <div class="header">
+        <div class="logo" @click='backToHomepage'>
+            <img src='static/img/logo.svg'/>
+            <div class='product-name'>
+				<span>The<br>Weather<br>Helper</span>
+			</div>
+        </div>
+        <div class="user-info">
+            <el-dropdown trigger="click" @command="handleCommand">
+                <span class="el-dropdown-link">
+                    <img class="user-logo" src="../../../static/img/img.jpg">
+                    {{username}}
+                </span>
+                <el-dropdown-menu slot="dropdown">
+                    <el-dropdown-item command="userCenter">User Center</el-dropdown-item>
+                    <el-dropdown-item command="loginout">Log out</el-dropdown-item>
+                </el-dropdown-menu>
+            </el-dropdown>
+        </div>
+    </div>
+</template>
+<script>
+    import AppModel from '../page/AppModel.js'
+    export default {
+        data() {
+            return {
+                name: 'shaw'
+            }
+        },
+        computed:{
+            username(){
+                let username = sessionStorage.getItem('ms_username');
+                return username ? username : this.name;
+            }
+        },
+        methods:{
+            handleCommand(command) {
+                if(command == 'loginout'){
+                    sessionStorage.removeItem('ms_username')
+                    sessionStorage.removeItem('ms_userId')
+                    this.$router.push('/');
+                } else if (command == 'userCenter') {
+                    this.$router.push('/readme');
+                }
+            },
+            backToHomepage(){
+                this.$router.push('/')
+            }
+        }
+    }
+</script>
+<style scoped>
+    .header {
+        position: relative;
+        box-sizing: border-box;
+        width: 100%;
+        height: 70px;
+        font-size: 22px;
+        line-height: 70px;
+        color: #fff;
+    }
+    .header .logo{
+        display:block;
+        float:left;
+        margin:0 auto;
+        width:75px;
+        font-size: 10px;
+        line-height:10px;
+        margin-top:15px;
+        margin-left:30px;
+        cursor:pointer;
+    }
+
+	.header .logo .product-name {
+		font-weight: bold;
+        float:right;
+        margin-top:7px;
+        color:#5789B4;
+        filter: drop-shadow(-1.5px 1.5px 0px #120E52);
+	}
+
+	.header .logo img {
+        float:left;
+        max-width:20px;
+		filter: drop-shadow(-4px 4px 0px #120E52);
+	}
+    
+    .user-info {
+        float: right;
+        padding-right: 50px;
+        font-size: 16px;
+        color: #fff;
+    }
+    .user-info .el-dropdown-link{
+        position: relative;
+        display: inline-block;
+        padding-left: 50px;
+        color: #fff;
+        cursor: pointer;
+        vertical-align: middle;
+    }
+    .user-info .user-logo{
+        position: absolute;
+        left:0;
+        top:15px;
+        width:40px;
+        height:40px;
+        border-radius: 50%;
+    }
+    .el-dropdown-menu__item{
+        text-align: center;
+    }
+</style>

+ 19 - 0
src/components/common/Home.vue

@@ -0,0 +1,19 @@
+<template>
+    <div class="wrapper">
+        <v-head></v-head>
+        <v-sidebar></v-sidebar>
+        <div class="content">
+            <transition name="move" mode="out-in"><router-view></router-view></transition>
+        </div>
+    </div>
+</template>
+
+<script>
+    import vHead from './Header.vue';
+    import vSidebar from './Sidebar.vue';
+    export default {
+        components:{
+            vHead, vSidebar
+        }
+    }
+</script>

+ 87 - 0
src/components/common/Sidebar.vue

@@ -0,0 +1,87 @@
+<template>
+    <div class="sidebar">
+        <el-menu :default-active="onRoutes" class="el-menu-vertical-demo" unique-opened router>
+            <template v-for="item in items">
+                <template v-if="item.subs">
+                    <el-submenu :index="item.index">
+                        <template slot="title"><i :class="item.icon"></i>{{ item.title }}</template>
+                        <el-menu-item v-for="(subItem,i) in item.subs" :key="i" :index="subItem.index" class="el-submenu-item-css">{{ subItem.title }}
+                        </el-menu-item>
+                    </el-submenu>
+                </template>
+                <template v-else>
+                    <el-menu-item :index="item.index" class="el-menu-item-css">
+                        <i :class="item.icon"></i>{{ item.title }}
+                    </el-menu-item>
+                </template>
+            </template>
+        </el-menu>
+    </div>
+</template>
+
+<script>
+    export default {
+        data() {
+            return {
+                items: [
+                    {
+                        icon: 'el-icon-setting',
+                        index: 'readme',
+                        title: 'Readme'
+                    },
+                    {
+                        icon: 'el-icon-setting',
+                        index: 'userCenter',
+                        title: 'Setting',
+                        subs: [
+                            {
+                                index: 'modifyUser',
+                                title: 'Modify userInfo'
+                            },
+                            {
+                                index: 'ModifyPassword',
+                                title: 'Modify password'
+                            }
+                        ]
+                    }
+                ]
+            }
+        },
+        computed:{
+            onRoutes(){
+                return this.$route.path.replace('/','');
+            }
+        }
+    }
+</script>
+
+<style scoped>
+    .sidebar{
+        display: block;
+        position: absolute;
+        width: 250px;
+        left: 0;
+        top: 70px;
+        bottom:0;
+        background-color: #2E363F;
+    }
+    .el-menu-vertical-demo{
+        background-color: #FBF9EB;
+    }
+    .sidebar > ul {
+        height:100%;
+    }
+    .el-menu-item-css{
+        color:#48576B;
+    }
+    .el-menu-item-css:hover{
+        background-color:#FFE082;
+    }
+    .el-submenu-item-css{
+        background-color:#FBF9EB;
+    }
+    .el-submenu-item-css:hover{
+        background-color:#FFE082;
+    }
+
+</style>

+ 33 - 0
src/components/entities/DailyForecastEntity.js

@@ -0,0 +1,33 @@
+export default class {
+
+	constructor(hourlyRecordsForOneDay){
+		this.hourlyRecords = hourlyRecordsForOneDay
+	}
+
+	highTemperature(){
+		return this.hourlyRecords
+		.reduce((maximumAttainedTemperatureYet,hourlyRecord) => {
+			if(hourlyRecord.temperature > maximumAttainedTemperatureYet.temperature)
+				return hourlyRecord
+			else return maximumAttainedTemperatureYet
+		}).temperature
+	}
+
+	lowTemperature(){
+		return this.hourlyRecords
+		.reduce((maximumAttainedTemperatureYet,hourlyRecord) => {
+			if(hourlyRecord.temperature < maximumAttainedTemperatureYet.temperature)
+				return hourlyRecord
+			else return maximumAttainedTemperatureYet
+		}).temperature
+	}
+
+	date(){
+		return this.hourlyRecords[0].date
+	}
+
+	condition(){
+		return this.hourlyRecords[0].condition
+	}
+
+}

+ 23 - 0
src/components/models/CompositeDashboardModel.js

@@ -0,0 +1,23 @@
+import createObservable from './Observable.js'
+
+import CurrentWeatherModel from '../models/CurrentWeatherModel.js'
+import HourForecastModel from '../models/HourForecastModel.js'
+import DayForecastModel from '../models/DayForecastModel.js'
+
+let CompositeDashboardModel = {	
+	__proto__: createObservable(),
+	countdown: 3,
+	modelChangedState(){
+		this.countdown--;
+		if(this.countdown === 0) {
+			this.countdown = 3;			
+			this.inform();
+		}
+	}
+}
+
+CurrentWeatherModel.onchange(() => { CompositeDashboardModel.modelChangedState() });
+HourForecastModel.onchange(() => { CompositeDashboardModel.modelChangedState() });
+DayForecastModel.onchange(() => { CompositeDashboardModel.modelChangedState() });
+
+export default CompositeDashboardModel;

+ 21 - 0
src/components/models/CurrentWeatherModel.js

@@ -0,0 +1,21 @@
+import createObservable from './Observable.js'
+
+export default {
+	__proto__: createObservable(),
+	basicInfo: {
+            city: "N/A",
+			temperature: "N/A",
+			minTemperature: "N/A",
+			maxTemperature: "N/A",
+			condition: "N/A",
+			humidity: "N/A",
+			wind_speed: "N/A"
+	           },
+	updateBasicWeatherInfo(updatedWeather){
+		this.basicInfo = updatedWeather;
+		this.inform();
+	},
+	basicWeatherInfo(){
+		return this.basicInfo;
+	}
+}

+ 30 - 0
src/components/models/DayForecastModel.js

@@ -0,0 +1,30 @@
+import createObservable from './Observable.js'
+import DailyForecast from '../entities/DailyForecastEntity.js'
+
+function retrieveDayNameByDate(date){
+	return Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(date)
+}
+
+export default {
+	__proto__: createObservable(),
+	hourWeatherPerDayForecastMatrix: [],
+	updateDailyWeatherForecast(hourWeatherPerDayForecastMatrix){
+		this.hourWeatherPerDayForecastMatrix = hourWeatherPerDayForecastMatrix;
+		this.inform();
+	},
+	getDailyForecaseForFirstN(count){
+		return this.hourWeatherPerDayForecastMatrix
+				   .map((hourlyForecastForOneDay) => {
+						let dailyForecast = new DailyForecast(hourlyForecastForOneDay)
+						return {
+							day: retrieveDayNameByDate(dailyForecast.date()),
+							highTemperature: dailyForecast.highTemperature(),
+							lowTemperature: dailyForecast.lowTemperature(),
+							condition: dailyForecast.condition()
+		    			}
+					}).slice(0, count);
+	},
+	getDailyForecast(){
+		return this.forecastsForEachComingDay;
+	}
+}

+ 17 - 0
src/components/models/HourForecastModel.js

@@ -0,0 +1,17 @@
+import createObservable from './Observable.js'
+
+export default {
+	__proto__: createObservable(),
+	hourlyForecast: [],
+	units: {
+		temperature: 'celcius',
+		timeFormat: '24-hour'
+	},
+	updateHourlyForecast(hourlyWeatherForecast){
+		this.hourlyForecast = hourlyWeatherForecast;
+		this.inform();
+	},
+	getHourlyForecast(){
+		return this.hourlyForecast;
+	}
+}

+ 12 - 0
src/components/models/Observable.js

@@ -0,0 +1,12 @@
+export default function() {
+	return {
+		handlers: [],
+		onchange(handler){
+			this.handlers.push(handler);
+		},
+		inform(){
+			for(let handler of this.handlers)
+				handler();
+		}
+	}
+}

+ 49 - 0
src/components/page/AppModel.js

@@ -0,0 +1,49 @@
+import WeatherConfiguration from './WeatherConfiguration.js'
+
+import CurrentWeatherModel from '../models/CurrentWeatherModel.js'
+import DayForecastModel from '../models/DayForecastModel.js'
+import HourForecastModel from '../models/HourForecastModel.js'
+
+import WeatherApi from './OpenWeatherApiAdapter.js'
+
+function tabulateHourWeatherIntoDayHourMatrix(hourlyRecords){
+	let currentIndex = -1
+	let currentDay
+	let array = []
+	hourlyRecords
+	.forEach((hourlyRecord) => {
+		if(hourlyRecord.date.getDate() !== currentDay) {
+			currentDay = hourlyRecord.date.getDate()
+			currentIndex++
+			array[currentIndex] = []
+		}
+		array[currentIndex].push(hourlyRecord)
+	})
+	return array
+}
+
+export default {
+	assignCityAndUnitFamily(){
+		WeatherConfiguration.updateCity();
+		WeatherConfiguration.updateUnitFamily();
+		this.refresh();
+	},
+	refresh(){
+        let city = sessionStorage.getItem('ms_city')
+        let unitFamily = sessionStorage.getItem('ms_unit')
+		WeatherApi.retrieveCurrentWeatherInfo(city,unitFamily)
+		.then((result) => {
+			CurrentWeatherModel.updateBasicWeatherInfo(result)
+		})
+		WeatherApi.retrieveWeatherTemperatureAndConditionForNextHours(city,unitFamily,5)
+		.then((result) => {
+			HourForecastModel.updateHourlyForecast(result)
+		})
+		WeatherApi.retrieveWeatherTemperatureAndConditionForNextHours(city,unitFamily,30)
+		.then((result) => {
+			DayForecastModel.updateDailyWeatherForecast(
+				tabulateHourWeatherIntoDayHourMatrix(result)
+			)
+		})
+	}
+}

+ 20 - 0
src/components/page/Card.vue

@@ -0,0 +1,20 @@
+<template>
+	<div class='card'>
+		<slot>
+		</slot>
+	</div>
+</template>
+<script>
+export default {
+	name: 'card-view'
+}
+</script>
+<style>
+.card {
+	box-sizing: border-box;
+	height: 100%;
+	width: 100%;	
+	border-radius: 20px;
+	border: 2.6px solid #5789B4;
+}
+</style>

+ 28 - 0
src/components/page/CenteredFlexView.vue

@@ -0,0 +1,28 @@
+<template>
+	<div class='flex-view' :style='flexDirectionStyle'>
+		<slot>
+		</slot>
+	</div>
+</template>
+<script>
+export default {
+	props: ['direction'],
+	name: 'centered-flex-view',
+	computed: {
+		flexDirectionStyle(){
+			if(this.direction === 'horizontal')
+				return 'flex-direction: row'
+			else return 'flex-direction: column'
+		}
+	}
+}
+</script>
+<style scoped>
+.flex-view{	
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	height: 100%;
+	width:100%;
+}
+</style>

+ 237 - 0
src/components/page/ConfigurationScreen.vue

@@ -0,0 +1,237 @@
+<template>
+	<div class='config-screen'>
+        <div class='top-right-buttons'>
+		    <button class='top-button' @click='signup()'>Sign up</button>
+            <button class='top-button' @click='login()'>Login</button>
+        </div>
+		<div class='branding'>
+			<img src='static/img/logo.svg'  />
+			<div class='product-name'>
+				<span>The<br>Weather<br>Helper</span>
+			</div>
+		</div>
+		<div class='controls'>
+			<div class='border-circled'>			
+				<input class='city-input' v-model='city' placeholder='City (i.e. Bristol)'></input>
+                <button class='search-button' @click='changeCityForCurrentWeatherModel()'>
+                    <img src='/static/img/send.svg'/>
+                </button>
+			</div>
+			<toggle-view class='temperature-unit-selecter' @optionChanged='unitOptionChanged' first-option='°C' second-option='°F' />
+		</div>
+	</div>
+</template>
+
+<script type="text/javascript">
+
+import AppModel from './AppModel.js';
+import ToggleView from './ToggleView.vue'
+import Weather from './Weather.vue'
+import CompositeDashboardModel from '../models/CompositeDashboardModel.js'
+
+let unitToFamilyMap = {
+	'°C': 'metric',
+	'°F': 'imperial'
+}
+
+export default {
+  name: 'configuration-screen',
+  components: {
+	'toggle-view': ToggleView
+  },
+  data () {
+    return {
+     	city: '',
+	    unit: ''
+    }
+  },
+  methods: {
+	signup: function () {
+		this.$router.push("/register").catch(error => {
+			if (error.name != "NavigationDuplicated") {
+				throw error;
+            }
+        });
+
+	},
+
+	login: function () {
+		this.$router.push("/login").catch(error => {
+			if (error.name != "NavigationDuplicated") {
+				throw error;
+            }
+        });
+
+	},
+
+	changeCityForCurrentWeatherModel(){
+        sessionStorage.setItem('ms_city',this.city)
+        sessionStorage.setItem('ms_unit',unitToFamilyMap[this.unit])
+		AppModel.assignCityAndUnitFamily();
+	},
+	unitOptionChanged(newOption){
+		this.unit = newOption;
+	}
+  },
+  beforeRouteLeave(to, from, next) {
+	    if (to.path == "/index") {
+		  to.meta.keepAlive = true;
+	    } else {
+			 to.meta.keepAlive = false;
+		}
+		next();
+	},
+  computed: {
+	unitFamily(){
+		return unitToFamilyMap[this.unit];
+	}
+  },
+  mounted(){
+			CompositeDashboardModel.onchange(() => { this.$router.push({path: '/weather'})
+})
+}
+  
+}
+</script>
+
+<style>
+	.config-screen {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: space-evenly;
+        
+	}
+
+	.config-screen .branding *{
+		vertical-align: middle;
+        align-items: center;
+	}
+
+	.config-screen .branding .product-name {		
+		font-weight: bold;
+		display: inline-block;
+        margin-top:60px;
+	}
+
+	.config-screen .branding img {
+        margin-top:60px;
+		filter: drop-shadow(-4px 4px 0px #120E52);
+	}
+    
+    .dashboard-main .branding .emblem-image-wrapper img {
+	   display: block;
+	   text-align: right;
+    
+}
+
+	.config-screen .branding .product-name span{
+		display:block;
+		margin-left: 30px;
+		color: #5789B4;
+		font-size:400%;
+		text-shadow: -0.07em 0.07em 0px #120E52;
+		line-height: 100%;
+	}
+
+	.config-screen .controls * {
+		vertical-align: middle;
+        
+	}
+
+	.config-screen .controls .border-circled {
+        width: 500px;
+        border-bottom: 2px solid #5789B4;
+	}
+
+	.config-screen .controls .city-input {
+		box-sizing: content-box;
+		width: 400px;
+  		font-size: 133%;
+		height: 30px;
+ 		padding-left: 6px;
+        padding-top: 3px;
+		border: none;
+		outline: none;
+		background-color: transparent;
+		color: #5789B4;
+		text-align: center;
+        
+	}
+    
+    .config-screen .controls button {
+        background-color: transparent;
+        border-style:none;
+        outline:none;
+    }
+    
+    .config-screen .controls .search-button img {
+        max-width:40px;
+        max-height:40px;
+        border-style:none;
+    }
+    
+    .config-screen .controls .search-button img:hover {
+        cursor:pointer;
+        filter: invert(12%) sepia(23%) saturate(5451%) hue-rotate(230deg) brightness(88%) contrast(125%);
+    }
+    
+    .config-screen .controls button:hover{
+        cursor:pointer;
+    }
+    
+
+	.config-screen .controls .city-input::placeholder {
+		color: #5789B4;
+		opacity: 0.7;
+	}
+
+	.config-screen .controls .temperature-unit-selecter {
+		display: block;
+		text-align: center;
+		margin-top: 30px;
+	}
+     .top-right-buttons{
+        display:inline-block;
+        position: fixed;
+        top: 2%;
+        right: 7%;
+    }
+    
+    .top-right-buttons button{
+        height: 50px;
+        background-color: transparent;
+        border-style:none;
+        outline: none;
+        color:#5789B4;
+        font-size: 12px;
+        font-weight: bold;
+        margin:8px;
+    }
+    .top-right-buttons button:hover{
+        height: 20px;
+        background-color: #FFCB3C;
+        border-radius:3px;
+        color:#FBF9EB;
+        cursor:pointer;
+    }
+    
+    .config-screen{ 
+        position:absolute;
+        top:0;
+        left:0;
+        height:100%;
+        width:100%;
+        background-image:url("/static/img/bg.png"); 
+        background-position: center 0;
+        background-repeat: no-repeat;
+        background-attachment:fixed;
+        background-size: cover;
+        -webkit-background-size: cover;/* 兼容Webkit内核浏览器如Chrome和Safari */
+        -o-background-size: cover;/* 兼容Opera */
+        zoom: 1;    
+        }
+    
+    
+</style>
+

+ 99 - 0
src/components/page/CurrentWeatherConditionCard.vue

@@ -0,0 +1,99 @@
+<template>
+	<card-view class='current-weather-card'>
+		<centered-flex>
+			<span class='card-title' >{{ headerText }}</span>
+			<span class='current-weather'>{{ currentTemperature }}°</span>
+			<div class='current-weather-details'>
+				<weather-condition-art preset='medium' :condition='weatherCondition' class='current-weather-condition-image'
+				/><div class='current-weather-extras'>
+					<span class='current-temp-high-low' >{{ tempHigh }}°/{{ tempLow }}°</span>
+					<span>Humidity: {{ humidity }}</span>
+					<span>Wind: {{ windSpeed }}</span>
+				</div>
+			</div>
+		</centered-flex>
+	</card-view>
+</template>
+<script>
+
+import Card from './Card.vue'
+import CenteredFlexView from './CenteredFlexView.vue'
+import WeatherConditionArt from './WeatherConditionArt.vue'
+
+export default {
+	name: 'weather-condition-card',
+	props: ['header-text',
+		'current-temperature',
+		'temp-high',
+		'temp-low',
+		'weather-condition',
+		'humidity',
+		'wind-speed'],
+	components: {
+		'card-view': Card,
+		'weather-condition-art': WeatherConditionArt,
+		'centered-flex': CenteredFlexView
+	}
+}	
+</script>
+<style scoped>
+* {
+	box-sizing: border-box;
+}
+
+.current-weather-card{
+	box-sizing: border-box;
+	display:inline-block;
+	padding: 18.01% 11.4% 18.01% 13.2%;
+    background-color: #FFE082;
+}
+
+.current-weather-card .card-title {
+	display: block;
+	font-size: 200%;
+	text-align: center;
+	line-height:100%;
+	font-weight: 550;
+}
+
+.current-weather-card .current-weather {
+	line-height:1;
+	display: block;
+	font-size:900%;
+	font-weight: bold;
+	text-shadow: var(--text-large-temperature-double-y) var(--text-large-temperature-double-x) 0 var(--weather-double-color);
+}
+
+.current-weather-card .current-weather-details {
+	text-align: center;
+}
+
+.current-weather-card .current-weather-details > * {
+	display:inline-block;
+	vertical-align: middle;
+}
+
+.current-weather-card .current-weather-details .current-weather-condition-image {
+	max-width: 34%;
+}
+
+.current-weather-card .current-weather-details .current-weather-condition-image img{
+	width:100%;
+	filter: drop-shadow(var(--weather-condition-image-double-y) var(--weather-condition-image-double-x) 0 var(--weather-double-color));
+}
+
+.current-weather-card .current-weather-details .current-weather-extras{
+	margin-left: 4%;
+}
+
+.current-weather-card .current-weather-details .current-weather-extras span{
+	display:block;
+	line-height:120%;
+	text-align:initial;
+}
+
+.current-weather-card .current-weather-details .current-weather-extras .current-temp-high-low{
+	font-size:200%;
+    
+}
+</style>

+ 46 - 0
src/components/page/DayWeatherForecast.vue

@@ -0,0 +1,46 @@
+<template>
+	<card-view>
+		<centered-flex class='another-day-weather-card'>
+			<span class='card-title' >{{ cardTitle }}</span>
+			<weather-condition-art preset='large' :condition='weatherCondition'/>
+			<div class='basic-weather-info'>
+				<span class='current-temp-high-low' >{{ tempHigh }}°/{{ tempLow }}°</span>
+			</div>
+		</centered-flex>
+	</card-view>
+</template>
+<script>
+import Card from './Card.vue'
+import CenteredFlexView from './CenteredFlexView.vue'
+import WeatherConditionArt from './WeatherConditionArt.vue'
+
+export default {
+	name: 'day-weather-forecast',
+	props: ['card-title','temp-low','temp-high','weather-condition'],
+	components: {
+		'card-view': Card,
+		'centered-flex': CenteredFlexView,
+		'weather-condition-art': WeatherConditionArt
+	}
+}
+</script>
+<style scoped>
+* {
+	box-sizing: border-box;
+    
+}
+
+.another-day-weather-card {
+	padding: 14.1% 19.8% 14.1% 18.9%;
+	text-align: center;
+}
+
+.another-day-weather-card .card-title{
+	line-height: 100%;
+}
+
+.another-day-weather-card .basic-weather-info .current-temp-high-low {
+	font-size: 110%;
+}
+
+</style>

+ 124 - 0
src/components/page/Identify.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="s-canvas">
+    <canvas id="s-canvas" :width="contentWidth" :height="contentHeight"></canvas>
+  </div>
+</template>
+<script>
+  export default{
+    name: 'SIdentify',
+    props: {
+      identifyCode: {
+        type: String,
+        default: '1234'
+      },
+      fontSizeMin: {
+        type: Number,
+        default: 16
+      },
+      fontSizeMax: {
+        type: Number,
+        default: 40
+      },
+      backgroundColorMin: {
+        type: Number,
+        default: 180
+      },
+      backgroundColorMax: {
+        type: Number,
+        default: 240
+      },
+      colorMin: {
+        type: Number,
+        default: 50
+      },
+      colorMax: {
+        type: Number,
+        default: 160
+      },
+      lineColorMin: {
+        type: Number,
+        default: 40
+      },
+      lineColorMax: {
+        type: Number,
+        default: 180
+      },
+      dotColorMin: {
+        type: Number,
+        default: 0
+      },
+      dotColorMax: {
+        type: Number,
+        default: 255
+      },
+      contentWidth: {
+        type: Number,
+        default: 112
+      },
+      contentHeight: {
+        type: Number,
+        default: 38
+      }
+    },
+    methods: {
+      randomNum (min, max) {
+        return Math.floor(Math.random() * (max - min) + min)
+      },
+      randomColor (min, max) {
+        let r = this.randomNum(min, max)
+        let g = this.randomNum(min, max)
+        let b = this.randomNum(min, max)
+        return 'rgb(' + r + ',' + g + ',' + b + ')'
+      },
+      drawPic () {
+        let canvas = document.getElementById('s-canvas')
+        let ctx = canvas.getContext('2d')
+        ctx.textBaseline = 'bottom'
+        ctx.fillStyle = this.randomColor(this.backgroundColorMin, this.backgroundColorMax)
+        ctx.fillRect(0, 0, this.contentWidth, this.contentHeight)
+        for (let i = 0; i < this.identifyCode.length; i++) {
+          this.drawText(ctx, this.identifyCode[i], i)
+        }
+        this.drawLine(ctx)
+        this.drawDot(ctx)
+      },
+      drawText (ctx, txt, i) {
+        ctx.fillStyle = this.randomColor(this.colorMin, this.colorMax)
+        ctx.font = this.randomNum(this.fontSizeMin, this.fontSizeMax) + 'px SimHei'
+        let x = (i + 1) * (this.contentWidth / (this.identifyCode.length + 1))
+        let y = this.randomNum(this.fontSizeMax, this.contentHeight - 5)
+        var deg = this.randomNum(-45, 45)
+        ctx.translate(x, y)
+        ctx.rotate(deg * Math.PI / 180)
+        ctx.fillText(txt, 0, 0)
+        ctx.rotate(-deg * Math.PI / 180)
+        ctx.translate(-x, -y)
+      },
+      drawLine (ctx) {
+        for (let i = 0; i < 8; i++) {
+          ctx.strokeStyle = this.randomColor(this.lineColorMin, this.lineColorMax)
+          ctx.beginPath()
+          ctx.moveTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
+          ctx.lineTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
+          ctx.stroke()
+        }
+      },
+      drawDot (ctx) {
+        for (let i = 0; i < 100; i++) {
+          ctx.fillStyle = this.randomColor(0, 255)
+          ctx.beginPath()
+          ctx.arc(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight), 1, 0, 2 * Math.PI)
+          ctx.fill()
+        }
+      }
+    },
+    watch: {
+      identifyCode () {
+        this.drawPic()
+      }
+    },
+    mounted () {
+      this.drawPic()
+    }
+  }
+</script>

+ 178 - 0
src/components/page/Login.vue

@@ -0,0 +1,178 @@
+<template>
+    <div class="login-wrap">
+        <div class="ms-title">Login</div>
+        <div class="ms-login">
+            <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="0px" class="demo-ruleForm">
+                <div v-if="errorInfo">
+                    <span>{{errInfo}}</span>
+                </div>
+                <el-form-item prop="name">
+                    <el-input v-model="ruleForm.name" placeholder="Username" ></el-input>
+                </el-form-item>
+                <el-form-item prop="password">
+                    <el-input type="password" placeholder="Password" v-model="ruleForm.password" @keyup.enter.native="submitForm('ruleForm')"></el-input>
+                </el-form-item>
+                <el-form-item  prop="validate">
+                    <el-input v-model="ruleForm.validate" class="validate-code" placeholder="" ></el-input>
+                    <div class="code" @click="refreshCode">
+                        <s-identify :identifyCode="identifyCode"></s-identify>
+                    </div>
+                </el-form-item>
+                <div class="login-btn">
+                    <el-button type="primary" @click="submitForm('ruleForm')">Submit</el-button>
+                </div>
+                <p class="register" @click="handleCommand()">Create an Account</p>
+            </el-form>
+        </div>
+    </div>    
+</template>
+
+<script>
+import axios from 'axios';
+    export default {
+        name: 'login',
+        data() {
+            return {
+                identifyCodes: "1234567890",
+                identifyCode: "",
+                errorInfo : false,
+                ruleForm: {
+                    name: '',
+                    password: '',
+                    validate: ''                    
+                },
+                rules: {
+                    name: [
+                        { required: true, message: 'please enter username', trigger: 'blur' }
+                    ],
+                    password: [
+                        { required: true, message: 'please enter password', trigger: 'blur' }
+                    ],
+                    validate: [
+                        { required: true, message: 'please enter verification code', trigger: 'blur' }
+                    ]
+                }
+            }
+        },
+        mounted() {
+            this.identifyCode = "";
+            this.makeCode(this.identifyCodes, 4);
+        },
+        methods: {
+            submitForm(formName) {
+                // debounceAjax(formName)
+                const self = this;
+                self.$refs[formName].validate((valid) => {
+                    if (self.ruleForm.validate != this.identifyCode){
+                        alert('Please enter true verification code!')
+                    }
+                    else if (self.ruleForm.validate== this.identifyCode) {                      
+                        axios.post('/api/user/login',JSON.stringify(self.ruleForm))
+                        .then((response) => {
+                            console.log(response);
+                            if (response.data == -1) {
+                                self.errorInfo = true;
+                                self.errInfo = 'no such user';
+                                console.log('Invalid username or password.')
+                            } else if (response.data == 0) {
+                                console.log('Invalid username or password.')
+                                self.errorInfo = true;
+                                self.errInfo = 'Invalid username or password.';
+                            } else if (response.status == 200) {
+                                sessionStorage.setItem('ms_userId',self.ruleForm.name);
+                                sessionStorage.setItem('ms_username',self.ruleForm.name);
+                                sessionStorage.setItem('ms_user',JSON.stringify(self.ruleForm));
+                                self.$router.push('/readme');
+                                console.log(JSON.stringify(self.ruleForm)); 
+                            }                            
+                        }).then((error) => {
+                            console.log(error);
+                        })
+                    } else {
+                        console.log('error submit!!');
+                        return false;
+                    }
+                });
+            },
+            handleCommand() {
+                this.$router.push('/register');
+            },
+            randomNum(min, max) {
+                return Math.floor(Math.random() * (max - min) + min);
+            },
+            refreshCode() {
+                this.identifyCode = "";
+                this.makeCode(this.identifyCodes, 4);
+            },
+            makeCode(o, l) {
+                for (let i = 0; i < l; i++) {
+                    this.identifyCode += this.identifyCodes[
+                    this.randomNum(0, this.identifyCodes.length)
+                    ];
+                }
+            }
+        }
+    }
+</script>
+
+<style scoped>
+    .login-wrap{
+        position: relative;
+        width:100%;
+        height:100%;
+        
+    }
+    .ms-title{
+        position: absolute;
+        top:50%;
+        width:100%;
+        margin:0 auto;
+        margin-top: -230px;
+        text-align: center;
+        font-size:30px;
+        color: #5789B4;
+        text-shadow: -0.05em 0.05em 0px #120E52;
+    }
+    .ms-login{
+        position: absolute;
+        left:50%;
+        top:50%;
+        width:300px;
+        height:240px;
+        margin:-150px 0 0 -190px;
+        padding:40px;
+        background: #fff;
+        -webkit-box-shadow: #98B6CA 0px 0px 4px;
+        -moz-box-shadow: #98B6CA 0px 0px 4px;
+        box-shadow: #98B6CA 0px 0px 4px;
+        border-radius:5px;
+    }
+    .ms-login span {
+        color: red;
+    }
+    .login-btn{
+        text-align: center;
+    }
+    .login-btn button{
+        width:100%;
+        height:36px;
+    }
+    .code {
+        width: 112px;
+        height: 35px;
+        border: 1px solid #ccc;
+        float: right;
+        border-radius: 2px;
+    }
+    .validate-code {
+        width: 136px;
+        float: left;
+    }
+    .register {
+        font-size:14px;
+        line-height:30px;
+        color:#999;
+        cursor: pointer;
+        float:right;
+    }
+</style>

+ 16 - 0
src/components/page/MeasureDependentUnitsCorrespondent.js

@@ -0,0 +1,16 @@
+let unitFamiliesToMeasureUnitsMap = {
+	imperial: {
+		temperature: '°F',
+		speed: 'mph'
+	},
+	metric: {
+		temperature: '°C',
+		speed: 'm/s'
+	}
+}
+
+export default {
+	retrieveUnitFamilyDependentMeasureUnit(unitFamily,measure){
+		return unitFamiliesToMeasureUnitsMap[unitFamily][measure];
+	}
+}

+ 14 - 0
src/components/page/MeasureIndependentUnitsCorrespondent.js

@@ -0,0 +1,14 @@
+let unitSystemIndependentMeasuresToUnitsMap = {
+	timeFormat: '24-hour',
+	precipitation: 'mm',
+	humidity: '%'
+}
+
+export default {
+	isUnitSystemIndependentMeasure(measure){
+		return Object.keys(unitSystemIndependentMeasuresToUnitsMap).includes(measure)
+	},
+	retrieveUnitFamilyIndependentMeasureUnit(measure){
+		return unitSystemIndependentMeasuresToUnitsMap[measure]
+	}
+}

+ 99 - 0
src/components/page/ModifyPassword.vue

@@ -0,0 +1,99 @@
+<template>
+	<div>
+		<div class="crumbs">
+            <el-breadcrumb separator="/">
+                <el-breadcrumb-item><i class="el-icon-edit"></i> Personal center</el-breadcrumb-item>
+                <el-breadcrumb-item>Modify password</el-breadcrumb-item>
+            </el-breadcrumb>
+		</div>
+		<div class="userContent">
+			<el-form ref="form" :model="form" :rules="rules" label-width="80px">
+				<el-form-item prop="pass" label="password">
+					<el-input v-model="form.pass" type="password" placeholder="Please enter password"></el-input>
+				</el-form-item>
+				<el-form-item prop="checkPass" label="confirm password">
+					<el-input v-model="form.checkPass" type="password" placeholder="Please enter password again"></el-input>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" @click="onSubmit('form')">Submit</el-button>
+					<el-button @click="onCancle()">Cancel</el-button>
+				</el-form-item>
+			</el-form>
+		</div>
+	</div>
+</template>
+
+<script>
+    import axios from 'axios';
+	export default {
+		data() {
+			var validatePass = (rule, value, callback) => {
+				if(value === '') {
+					callback(new Error('Please enter password'));
+				} else {
+					if(this.form.checkPass !== '') {
+						this.$refs.form.validateField('checkPass');
+					}
+					callback();
+				}
+			};
+			var validatePass2 = (rule, value, callback) => {
+				if(value === '') {
+					callback(new Error('Please enter password again'));
+				} else if (value !== this.form.pass) {
+					callback(new Error('The two passwords you typed do not match'));
+				} else {
+					callback();
+				}
+			};
+            return {
+				form: {
+					pass: '',
+					checkPass: ''
+				},
+				rules: {
+					pass: [
+                        { validator: validatePass, trigger: 'blur'}
+                    ],
+                    checkPass: [
+                        { validator: validatePass2, trigger: 'blur' }
+                    ]
+				}
+			}
+        },
+        methods:{
+        	onSubmit(formName) {
+				const self = this;
+				let formData = {
+					id: sessionStorage.getItem('ms_userId'),
+					pass: self.form.pass,
+					checkPass: self.form.checkPass
+				};			
+				self.$refs[formName].validate((valid) => {
+                    if (valid) {
+                        axios.post('/api/user/modifyPassword',formData).then(function(response) {
+							console.log(response);
+							self.$router.push('/login');
+						}).then(function(error) {
+							console.log(error);
+						})
+                    } else {
+                        console.log('error submit!!');
+                        return false;
+                    }
+                });
+        	},
+        	onCancle() {
+        		this.$router.push('/userCenter');
+        	}        	
+        }
+	}
+</script>
+
+<style>
+	.userContent {
+		width: 400px;
+		margin: 0 auto;
+	}
+</style>
+   

+ 126 - 0
src/components/page/ModifyUser.vue

@@ -0,0 +1,126 @@
+<template>
+	<div>
+		<div class="crumbs">
+            <el-breadcrumb separator="/">
+                <el-breadcrumb-item><i class="el-icon-edit"></i> Personal center</el-breadcrumb-item>
+                <el-breadcrumb-item>Modify user details</el-breadcrumb-item>
+            </el-breadcrumb>
+		</div>
+		<div class="userContent">
+			<el-form ref="form" :model="form" :rules="rules" label-width="80px">
+				<el-form-item prop="name" label="Username">
+					<el-input v-model="form.name" disabled></el-input>
+				</el-form-item>
+				<el-form-item prop="account" label="Account">
+					<el-input v-model="form.account" disabled></el-input>
+				</el-form-item>
+				<el-form-item prop="email" label="Email">
+					<el-input v-model="form.email" placeholder="Please enter email address"></el-input>
+				</el-form-item>
+				<el-form-item prop="phone" label="Phone">
+					<el-input v-model="form.phone" placeholder="Please enter phone number"></el-input>
+				</el-form-item>
+				<el-form-item prop="card" label="ID">
+					<el-input v-model="form.card" placeholder="Please enter ID number"></el-input>
+				</el-form-item>
+				<el-form-item prop="birth" label="Birthday">
+					<el-col :span="24">
+						<el-date-picker type="date" placeholder="Select date" v-model="form.birth" value-format="yyyy-MM-dd" style="width: 100%;"></el-date-picker>
+					</el-col>
+				</el-form-item>
+				<el-form-item prop="sex" label="Sex">
+					<el-select class="select-sex" v-model="form.sex" placeholder="Please enter sex">
+						<el-option label="Male" value="man"></el-option>
+						<el-option label="Female" value="woman"></el-option>
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" @click="updateUserData('form')">Submit</el-button>
+					<el-button @click="onCancle()">Cancel</el-button>
+				</el-form-item>
+			</el-form>
+		</div>
+	</div>
+</template>
+
+<script>
+	import Util from '../../utils/utils';
+    import axios from 'axios';
+	export default {
+		data() {
+			var validateEmail = (rule, value, callback) => {
+				if (value === '') {
+					callback(new Error('Please enter email address'));
+				} else if (!Util.emailReg.test(this.form.email)){
+					callback(new Error('Please enter correct email address'));
+				} else {
+					callback();
+				}
+			};
+            return {
+				form: {					
+					email: '',
+					phone: '',
+					card: '',
+					birth: '',
+					sex: ''
+				},
+				rules: {
+                    email: [
+                        { required: true, message: 'Please enter email address',validator: validateEmail, trigger: 'blur' }
+                    ],
+                    phone: [
+                        {  required: true, message: 'Please enter phone number',trigger: 'blur' }
+                    ],
+                    card: [
+                        {  required: true, message: 'Please enter ID number',trigger: 'blur' }
+                    ],
+                    birth: [
+                        { required: true, message: 'Please enter birthday',type: 'date', trigger: 'blur' }
+                    ],
+                    sex: [
+                        { required: true, message: 'Please enter sex', trigger: 'blur' }]
+                }
+			}
+        },
+        methods:{		
+			updateUserData(formName) {
+				const self = this;
+				let formData = {
+					id: sessionStorage.getItem('ms_userId'),
+					email: self.form.email,
+					phone: self.form.phone,
+					card: self.form.card,
+					birth: self.form.birth,
+					sex: self.form.sex
+				};
+						
+				self.$refs[formName].validate((valid) => {
+                    if (valid) {
+                        axios.post('/api/user/updateUser',formData).then(function(response) {								
+							self.$router.push('/success');
+						}).then(function(error) {
+							console.log(error);
+						})
+                    } else {
+                        console.log('error submit!!');
+                        return false;
+                    }
+                });              
+			},
+        	onCancle() {
+        		 this.$router.push('/userCenter');
+        	}       	
+		}
+	}
+</script>
+
+<style>
+	.userContent {
+		width: 400px;
+		margin: 0 auto;
+	}
+	.select-sex {
+		width: 320px;
+	}
+</style>

+ 12 - 0
src/components/page/Observable.js

@@ -0,0 +1,12 @@
+export default function() {
+	return {
+		handlers: [],
+		onchange(handler){
+			this.handlers.push(handler);
+		},
+		inform(){
+			for(let handler of this.handlers)
+				handler();
+		}
+	}
+}

+ 46 - 0
src/components/page/OpenWeatherApiAdapter.js

@@ -0,0 +1,46 @@
+let apiKey = '0ca9d040ff56e1b286a929f6c5dd3f19';
+import WeatherInfoEnum from './WeatherInfoEnum.js';
+import axios from 'axios';
+
+
+function convertWeatherInfoToCondition(openWeatherMapWeatherCondition){
+	if(WeatherInfoEnum.hasOwnProperty(openWeatherMapWeatherCondition.toUpperCase()))
+		return WeatherInfoEnum[openWeatherMapWeatherCondition.toUpperCase()]
+	else return WeatherInfoEnum.UNKNOWN
+}
+
+export default {
+	retrieveCurrentWeatherInfo(cityName,unit){
+		return axios.get('https://api.openweathermap.org/data/2.5/weather?q='+cityName+'&appid='+apiKey+'&units='+unit,{crossDomain:true})
+		       .then((response) => {
+				return {
+                    city: cityName,
+					temperature: response.data.main.temp,
+					minTemperature: response.data.main.temp_min,
+					maxTemperature: response.data.main.temp_max,
+					condition: convertWeatherInfoToCondition(response.data.weather[0].main),
+					humidity: response.data.main.humidity,
+					wind_speed: response.data.wind.speed
+				}
+			}
+            )
+            .catch(function(error){
+                   alert('Sorry,we cannot find weather information about this city!')
+                   window.location.href=".."
+            }
+            );
+	},
+	retrieveWeatherTemperatureAndConditionForNextHours(cityName,unit,recordCount){
+		return axios.get('https://api.openweathermap.org/data/2.5/forecast?q='+cityName+'&appid='+apiKey+'&units='+unit,{crossDomain:true})
+		       .then((response) => {
+				let records = [];
+				for(let i = 0; i < recordCount;i++)
+					records.push({
+						date: new Date(response.data.list[i].dt*1000),
+						temperature: response.data.list[i].main.temp,
+						condition: convertWeatherInfoToCondition(response.data.list[i].weather[0].main),
+					});
+				return records;
+			})
+	}
+}

File diff suppressed because it is too large
+ 80 - 0
src/components/page/Readme.vue


+ 186 - 0
src/components/page/Register.vue

@@ -0,0 +1,186 @@
+<template>
+	<div class="login-wrap">
+        <div class="ms-title">Register</div>
+		<div class="userContent">
+			<el-form ref="form" :model="form" :rules="rules" label-width="100px">
+				<el-form-item prop="name" label="Username">
+					<el-input v-model="form.name" placeholder="Please enter username"></el-input>
+				</el-form-item>
+				<el-form-item prop="account" label="Account">
+					<el-input v-model="form.account" placeholder="Please enter account"></el-input>
+				</el-form-item>
+				<el-form-item prop="pass" label="Password">
+					<el-input v-model="form.pass" type="password" placeholder="Please enter password"></el-input>
+				</el-form-item>
+				<el-form-item prop="checkPass" label="Confirm password">
+					<el-input v-model="form.checkPass" type="password" placeholder="Please enter password again"></el-input>
+				</el-form-item>
+				<el-form-item prop="email" label="Email">
+					<el-input v-model="form.email" placeholder="Please enter email address"></el-input>
+				</el-form-item>
+				<el-form-item prop="phone" label="Phone">
+					<el-input v-model="form.phone" placeholder="Please enter phone number"></el-input>
+				</el-form-item>
+				<el-form-item prop="card" label="ID">
+					<el-input v-model="form.card" placeholder="Please enter ID card number"></el-input>
+				</el-form-item>
+				<el-form-item prop="birth" label="Birth">
+					<el-col :span="24">
+						<el-date-picker type="date" placeholder="Select date" v-model="form.birth" value-format="yyyy-MM-dd" style="width: 100%;"></el-date-picker>
+					</el-col>
+				</el-form-item>
+				<el-form-item prop="sex" label="Sex">
+					<el-select class="select-sex" v-model="form.sex" placeholder="Select sex">
+						<el-option label="Male" value="man"></el-option>
+						<el-option label="Female" value="woman"></el-option>
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+                    <div class="bottom-position">
+					   <el-button type="primary" @click="onSubmit('form')">Submit</el-button>
+					   <el-button @click="onCancle()">Cancle</el-button>
+                    </div>
+				</el-form-item>
+			</el-form>
+		</div>
+	</div>
+</template>
+
+<script>
+	import Util from '../../utils/utils';
+    import axios from 'axios';
+	export default {
+		data() {
+			var validatePass = (rule, value, callback) => {
+				if(value === '') {
+					callback(new Error('Please enter password'));
+				} else {
+					if(this.form.checkPass !== '') {
+						this.$refs.form.validateField('checkPass');
+					}
+					callback();
+				}
+			};
+			var validatePass2 = (rule, value, callback) => {
+				if(value === '') {
+					callback(new Error('Please enter password again'));
+				} else if (value !== this.form.pass) {
+					callback(new Error('The two passwords you typed do not match'));
+				} else {
+					callback();
+				}
+			};
+			var validateEmail = (rule, value, callback) => {
+				if (value === '') {
+					callback(new Error('Please enter email address'));
+				} else if (!Util.emailReg.test(this.form.email)){
+					callback(new Error('Please enter correct email address'));
+				} else {
+					callback();
+				}
+			};
+            return {
+				form: {
+					name: '',
+					account: '',					
+					pass: '',
+					checkPass: '',
+					email: '',
+					phone: '',
+					card: '',
+					birth: '',
+					sex: ''
+                },
+                rules: {
+                    name: [
+                        { required: true, message: 'Please enter username', trigger: 'blur' }
+                    ],
+                    account: [
+                        { required: true, message: 'Please enter account', trigger: 'blur' }
+                    ],
+                    pass: [
+                        { required: true,validator: validatePass, trigger: 'blur' }
+                    ],
+                    checkPass: [
+                        { required: true,validator: validatePass2, trigger: 'blur' }
+                    ],
+                    email: [
+                        { required: true,validator: validateEmail, trigger: 'blur' }
+                    ],
+                    phone: [
+                        { required: true, trigger: 'blur' }
+                    ],
+                    card: [
+                        { required: true, trigger: 'blur' }
+                    ],
+                    birth: [
+                        { required: true, message: 'Please enter birth date',type: 'date', trigger: 'blur' }
+                    ],
+                    sex: [
+                        { required: true, message: 'Please enter sex', trigger: 'blur' }
+                    ]
+                }
+			}
+        },
+        methods:{
+        	onSubmit(formName) {
+				const self = this;
+				self.$refs[formName].validate((valid) => {
+                    if (valid) {
+                        axios.post('/api/user/addUser',self.form).then(function(response) {
+							self.$router.push('/register-success');
+						}).then(function(error) {
+							console.log(error);
+						})
+                    } else {
+                        console.log('error submit!!');
+                        return false;
+                    }
+                });
+        	},
+        	onCancle() {
+        		this.$router.push('/login');
+			},
+			getDateTimes(str) {
+				var str = new Date(str);
+        		return str;
+			}       	
+        }
+	}
+</script>
+
+<style>
+    .ms-title{
+        width:100%;
+        margin-top: 20px;
+        margin-bottom: 15px;
+        text-align: center;
+        font-size:30px;
+        color: #5789B4;
+        margin-left: 35px;
+        text-shadow: -0.05em 0.05em 0px #120E52;
+
+    } 
+	.crumbs-register {
+		background-color: #324157;
+		height: 50px;
+		line-height: 50px;
+	}
+	.register-title {
+		line-height: 50px;
+		margin: 0 auto;
+    	width: 100px;
+    	font-size: 16px;
+	}	
+	.userContent {
+		width: 400px;
+		margin: 0 auto;
+        margin-top:30px;
+	}
+	.select-sex {
+		width: 320px;
+	}
+    .bottom-position{
+        margin-left: 50px;
+    }
+</style>

+ 78 - 0
src/components/page/RegisterSuccess.vue

@@ -0,0 +1,78 @@
+<template>
+	<div class="whole">
+		<div class="userContent2">
+			<div class="eidt-success">
+                <div class="show-success">
+                    <i class="el-icon-check"></i>&nbsp;&nbsp;<span>Congratulations! You've successfully registered!</span>
+                </div> 
+                <div class="click-login">
+                    <a href="#" @click="handleCommand()">Go to login</a>
+                </div>            	
+            </div>
+		</div>
+	</div>
+</template>
+
+<script>
+	export default {
+		data() {
+            return {
+				form: {
+
+				}
+			}
+        },
+        methods: {
+            handleCommand() {
+                this.$router.push('/login');
+            }
+        }	
+    }
+	
+</script>
+
+<style>
+    .whole{
+        position:absolute;
+        top:0;
+        left:0;
+        height:100%;
+        width:100%;
+        background-image:url("/static/img/bg.png"); 
+        background-position: center 0;
+        background-repeat: no-repeat;
+        background-attachment:fixed;
+        background-size: cover;
+        -webkit-background-size: cover;/* 兼容Webkit内核浏览器如Chrome和Safari */
+        -o-background-size: cover;/* 兼容Opera */
+        zoom: 1; 
+    }
+	.userContent2 {
+		width: 100%;
+		margin: 0 auto;
+		background-size: cover;
+        margin-top:250px;
+	}
+    .eidt-success {
+		margin: 0 auto;
+		position: relative;
+        margin-top:100px;
+		
+	}
+	.eidt-success .show-success {
+		position: absolute;
+		margin-left: -190px;
+		left: 50%;
+	}
+    .click-login {
+        position: absolute;
+        left: 50%;
+        top: 50px;
+    }
+	.eidt-success i {
+		color: #67C23A;
+	}
+	.eidt-success i,.eidt-success span {
+		font-size: 20px;
+	}
+</style>

+ 51 - 0
src/components/page/Success.vue

@@ -0,0 +1,51 @@
+<template>
+	<div class="main-content">
+		<div class="crumbs">
+			<el-breadcrumb separator="/">
+                <el-breadcrumb-item><i class="el-icon-menu"></i>&nbsp;&nbsp;Modified successfully</el-breadcrumb-item>
+            </el-breadcrumb>
+		</div>
+		<div class="eidt-success">
+			<div>
+				<i class="el-icon-check"></i>&nbsp;&nbsp;<span>Congratulations! You've successfully modified!</span>
+			</div>            	
+		</div>
+	</div>
+</template>
+
+<script>
+	export default {
+		data() {
+            return {
+				form: {
+
+				}
+			}
+		}	
+    }
+	
+</script>
+
+<style>
+	.main-content{
+		background-size: cover;
+		height: 100%;
+	}
+	.eidt-success {
+		margin: 0 auto;
+		position: relative;
+        top:100px;
+		
+	}
+	.eidt-success div {
+		position: absolute;
+		margin-left: -190px;
+		left: 50%;
+	}
+	.eidt-success i {
+		color: #67C23A;
+	}
+	.eidt-success i,.eidt-success span {
+		font-size: 15px;
+	}
+</style>

+ 51 - 0
src/components/page/TimeScaleWeatherConditionView.vue

@@ -0,0 +1,51 @@
+<template>
+	<card>
+		<centered-flex class='time-scale-weather' direction='horizontal'>
+			<centered-flex class='time-weather-record' v-for="timedWeather in timedWeatherCollection" :key='timedWeather.time'>
+				<span>{{ timedWeather.time }}</span>
+				<weather-condition-art preset='small' :condition='timedWeather.weatherCondition' />
+				<span>{{ timedWeather.temperature }}°</span>
+			</centered-flex>
+		</centered-flex>
+	</card>
+</template>
+<script>
+
+import CenteredFlexView from './CenteredFlexView.vue'
+import WeatherConditionArt from './WeatherConditionArt.vue'
+import Card from './Card.vue'
+
+export default {
+	name: 'time-scale-weather-condition',
+	props: ['timed-weather-collection'],
+	components: {
+		'centered-flex': CenteredFlexView,
+		'card': Card,
+		'weather-condition-art': WeatherConditionArt
+	}
+}
+</script>
+<style scoped>
+* {
+	box-sizing: border-box;
+    
+}
+
+.time-scale-weather {
+	font-weight: 500;
+	padding: 3.61%;
+}
+
+.time-scale-weather span {	
+	font-size: 150%;
+	line-height: 100%;
+}
+
+.time-scale-weather .time-weather-record {
+	width: 14.75%;
+}
+
+.time-scale-weather .weather-art-img{
+	width: 100%;
+}
+</style>

+ 68 - 0
src/components/page/ToggleView.vue

@@ -0,0 +1,68 @@
+<template>
+	<div>
+		<div class='toggle-container'>
+			<button 
+				@click='changeCurrentChosenOptionTo(firstOption)'
+				:disabled='isChosenOption(firstOption)'
+				:class="{ 'chosen-option': isChosenOption(firstOption),
+					  'not-chosen-option': !isChosenOption(firstOption) }">
+				{{ firstOption }}
+			</button
+			><button
+				@click='changeCurrentChosenOptionTo(secondOption)'
+				:disabled='isChosenOption(secondOption)'
+				:class="{ 'chosen-option': isChosenOption(secondOption),
+					  'not-chosen-option': !isChosenOption(secondOption) }">
+				{{ secondOption }}
+			</button>
+		</div>
+	</div>
+</template>
+
+<script>
+	export default {
+		name: 'toogle-view',
+		props: ['firstOption','secondOption'],
+		data(){
+			return {
+				currentChosenOption: this.firstOption,
+			}
+		},
+		methods: {
+			changeCurrentChosenOptionTo(newOption){
+				this.currentChosenOption = newOption
+				this.$emit('optionChanged',this.currentChosenOption);
+			},
+			isChosenOption(option){
+				return option === this.currentChosenOption;
+			}
+		},
+		mounted(){
+			this.$emit('optionChanged',this.currentChosenOption);
+		}
+	}
+</script>
+
+<style>
+	.toggle-container {
+		display: inline-block;
+		border: 2.6px solid #7DA5BF;
+		border-radius: 10px;
+        
+	}
+
+	.toggle-container button {
+		padding: 7px 9px 7px 9px;
+		background-color: transparent;
+		font-size: 120%;
+		outline: none;
+		color: #5789B4;
+		transition: opacity 0.3s;
+		font-weight: bold;
+		border: none;		
+	}
+
+	.not-chosen-option {
+		opacity: 0.5;
+	}
+</style>

+ 245 - 0
src/components/page/Weather.vue

@@ -0,0 +1,245 @@
+<template>
+	<div class='dashboard-variables dashboard-main text-color-main'>
+		<div class='top-bar-grid-cell'>
+			<div class='branding' @click='backToHomepage'>
+				<div class='emblem-image-wrapper'>
+					<img style='height:100%;filter: drop-shadow(-2px 2px 0 #003c87);' src='/static/img/logo.svg' />
+				</div>
+                <div class='brand-name-wrapper'>
+					<span>The</span>
+					<span>Weather</span>
+					<span>Helper</span>
+				</div>
+			</div>
+		</div>
+
+
+		<div class='top-bar-grid-cell'>
+			<button class='refresh-button' @click='refreshAllWeatherStats'>
+				Refresh
+			</button>
+		</div>
+        
+		<div class='dashboard-content'>
+			<div class='dashboard-content-grid'>
+				<weather-condition-card
+					style='grid-column-start: 1;grid-column-end: 3;grid-row-start: 1;grid-row-end: 3;'	
+                    :header-text='(currentWeatherModel.basicWeatherInfo().city).toUpperCase()' 
+					:current-temperature='Math.round(currentWeatherModel.basicWeatherInfo().temperature)' 
+					:temp-high='Math.round(currentWeatherModel.basicWeatherInfo().maxTemperature)'
+					:temp-low='Math.round(currentWeatherModel.basicWeatherInfo().minTemperature)' 
+					:weather-condition='currentWeatherModel.basicWeatherInfo().condition'
+					:humidity='Math.round(currentWeatherModel.basicWeatherInfo().humidity)+"%"'
+					:wind-speed="Math.round(currentWeatherModel.basicWeatherInfo().wind_speed) + 'm/s'"
+				/>
+				<day-weather-forecast v-for='one in dayForecastModel.getDailyForecaseForFirstN(4)'
+					:card-title='one.day'
+					:weather-condition='one.condition'
+					:temp-high='Math.round(one.highTemperature)'
+					:temp-low='Math.round(one.lowTemperature)'
+				/>
+				<time-scale-weather-condition 
+					style='grid-column-start: 3;grid-column-end: 6'
+					:timed-weather-collection="hourlyWeatherForecast"
+				/>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import AppModel from './AppModel.js'
+
+import CurrentWeatherConditionCard from './CurrentWeatherConditionCard.vue'
+import CenteredFlexView from './CenteredFlexView.vue'
+import DayWeatherForecast from './DayWeatherForecast.vue'
+import TimeScaleWeatherConditionView from './TimeScaleWeatherConditionView.vue'
+
+import CurrentWeatherModel from '../models/CurrentWeatherModel.js'
+import HourForecastModel from '../models/HourForecastModel.js'
+import DayForecastModel from '../models/DayForecastModel.js'
+
+export default {
+	name: 'weather-view',
+	components: {
+		'weather-condition-card': CurrentWeatherConditionCard,
+		'day-weather-forecast': DayWeatherForecast,
+		'centered-flex': CenteredFlexView,
+		'time-scale-weather-condition': TimeScaleWeatherConditionView
+	},
+	data(){
+		return {
+			currentWeatherModel: CurrentWeatherModel,
+			hourForecastModel: HourForecastModel,
+			dayForecastModel: DayForecastModel
+		}
+	},
+    mounted(){
+         window.addEventListener('beforeunload', this.refreshAllWeatherStats())
+    },
+	computed: {
+		hourlyWeatherForecast(){
+			return this.hourForecastModel.getHourlyForecast()
+			       .map((oneHourlyRecord) => {
+					return {
+						time: this.presentTimeInXXColon00Format(oneHourlyRecord.date.getHours()),
+						weatherCondition: oneHourlyRecord.condition,
+						temperature: Math.round(oneHourlyRecord.temperature)
+					}
+			})
+		}
+	},
+	methods: {
+		presentTimeInXXColon00Format(hours){
+			if(hours < 10)
+				return "0" + hours+ ":00";
+			else return hours + ":00";
+		},
+		refreshAllWeatherStats(){
+			AppModel.refresh()
+		},
+        backToHomepage(){
+            this.$router.push('/')
+        }
+}
+}
+
+</script>
+
+<style>
+.dashboard-variables {	
+	--weather-foreground-color: #5789B4;
+	--weather-double-color: #120E52;
+	--weather-background-color: #120E52;
+	--dashboard-content-width: 876px;
+	--dashboard-content-height: 363px;	
+	--dashboard-content-gap-value: 41px;
+	--dashboard-padding-top: 30px;
+	--dashboard-top-height: 40px;
+	--dashboard-font-size-relative-to-rem: 	83%;
+	--dashboard-top-buttons-border-radius: 7px;
+	--dashboard-change-settings-button-width: 106px;
+	--dashboard-refresh-button-width: 60px;
+	--dashboard-top-buttons-height: 27px;
+	--card-stroke-width: 1.5px;
+}
+
+.dashboard-main {
+	display:grid;
+	box-sizing: border-box;	
+	padding: var(--dashboard-padding-top) 0 0 0;
+	grid-template-columns: 1fr var(--dashboard-content-width) 1fr;	
+	grid-template-rows: var(--dashboard-top-height) 1fr;
+	grid-column-gap: var(--dashboard-content-gap-value);
+	font-size: var(--dashboard-font-size-relative-to-rem);
+	flex-direction: column;
+	justify-content: space-between;
+    
+}
+
+.dashboard-main .top-bar-grid-cell {
+	position: relative;
+    
+}
+
+
+.dashboard-main .top-bar-grid-cell button{
+	height: var(--dashboard-top-buttons-height);	
+	display:inline-block;
+	background-color: transparent;	
+	outline: none;
+	border: var(--card-stroke-width) solid var(--weather-foreground-color);
+	border-radius: var(--dashboard-top-buttons-border-radius);
+}
+
+.dashboard-main .branding {
+	display: flex;
+	justify-content: flex-end;
+	width: 100%;
+	height: 100%;
+    cursor:pointer;
+}
+
+.dashboard-main .branding .emblem-image-wrapper {
+	flex: auto;
+	display: flex;
+	justify-content: flex-end;
+	margin-right: 10px;
+}
+
+.dashboard-main .branding .emblem-image-wrapper img {
+	display: block;
+	text-align: right;
+    max-width:25px;
+    max-height:40px;
+}
+
+.dashboard-main .branding .brand-name-wrapper span {
+	display: block;
+	font-weight: bold;
+	font-size: 100%;
+	line-height: 100%;
+    color:#5789B4;
+}
+
+.dashboard-main .change-location-button {
+	width: var(--dashboard-change-settings-button-width);
+	position: absolute;
+	top: 50%;
+	right: 0;	
+ 	transform: translate(0, -50%);
+    font-weight:bold;
+	font-size: 92%;
+    color:#5789B4;
+}
+
+.dashboard-main .change-location-button:hover {
+	filter: invert(12%) sepia(23%) saturate(5451%) hue-rotate(230deg) brightness(88%) contrast(125%);
+    cursor:pointer;
+}
+    
+
+.dashboard-main .refresh-button {
+	width: var(--dashboard-refresh-button-width);
+	position: absolute;
+	top: 50%;
+	right: -150px;
+ 	transform: translate(0, -50%);
+	padding: 4px 4px 4px 7px;
+    font-weight:bold;
+	font-size: 92%;
+    color:#5789B4;
+}
+
+.dashboard-main .refresh-button:hover{
+    filter: invert(12%) sepia(23%) saturate(5451%) hue-rotate(230deg) brightness(88%) contrast(125%);
+    cursor:pointer;
+}
+
+
+.dashboard-main .dashboard-content {
+	display:flex;
+	align-items: center;
+	grid-column-start:2;
+	grid-column-end:3;
+    margin-top:80px;
+    padding:50px;
+    -webkit-box-shadow: #666 0px 0px 10px;
+    -moz-box-shadow: #666 0px 0px 10px;
+    box-shadow: #666 0px 0px 10px;
+    background: #fffef5;
+    border-radius:25px
+}
+
+.dashboard-main .dashboard-content .dashboard-content-grid {
+	display: grid;
+	height: var(--dashboard-content-height);
+	width: 100%;
+	grid-template-columns: repeat(6,1fr);
+	grid-template-rows: repeat(2,1fr);
+	grid-gap: var(--dashboard-content-gap-value);
+}
+
+
+
+</style>

+ 36 - 0
src/components/page/WeatherConditionArt.vue

@@ -0,0 +1,36 @@
+<template>
+<div :class='chosenStylingPreset'>
+	<img :src='appropriateConditionArt' />
+</div>
+</template>
+<script>
+
+let weatherConditionToConditionArtMap = {
+	'clear': require('../../../static/img/clear.svg'),
+	'clear_night': require('../../../static/img/clear_night.svg'),
+	'clouds': require('../../../static/img/clouds.svg'),
+	'few_clouds': require('../../../static/img/few_clouds.svg'),
+	'few_clouds_night': require('../../../static/img/few_clouds_night.svg'),
+	'rain': require('../../../static/img/rain.svg')
+}
+
+export default {
+	name:'weather-condition-art',
+	props: ['preset','condition'],
+	computed: {
+		chosenStylingPreset(){
+			return { 'weather-condition-art-large': this.preset === 'large',
+				 'weather-condition-art-medium': this.preset === 'medium',
+				 'weather-condition-art-small': this.preset === 'small' }
+		},
+		appropriateConditionArt(){
+			return weatherConditionToConditionArtMap[this.condition];
+		}
+	}
+}
+</script>
+<style scoped>
+img {
+	width: 100%;
+}
+</style>

+ 42 - 0
src/components/page/WeatherConfiguration.js

@@ -0,0 +1,42 @@
+import createObservable from './Observable.js'
+import MeasureIndependentUnitsCorrespondent from './MeasureIndependentUnitsCorrespondent.js'
+import MeasureDependentUnitsCorrespondent from './MeasureDependentUnitsCorrespondent.js'
+
+let unitFamiliesToMeasureUnitsMap = {
+	imperial: {
+		temperature: '°F',
+		speed: 'mph'
+	},
+	metric: {
+		temperature: '°C',
+		speed: 'm/s'
+	}
+}
+
+export default {
+	__proto__: createObservable(),
+	city: 'N/A',
+	unitFamily: 'N/A',
+	units: {
+		temperature: 'N/A',
+		speed: 'N/A',
+		timeFormat: '24-hour',
+		precipitation: 'mm',
+		humidity: '%'
+	},
+	updateCity(){
+		this.city = sessionStorage.getItem('ms_city');
+		this.inform();
+	},
+	updateUnitFamily(){
+		this.unitFamily = sessionStorage.getItem('ms_unit');
+		this.inform();
+	},
+	retrieveUnitForMeasure(measure){
+        this.city = sessionStorage.getItem('ms_city');
+        this.unitFamily = sessionStorage.getItem('ms_unit');
+		if(MeasureIndependentUnitsCorrespondent.isUnitSystemIndependentMeasure(measure))
+			return MeasureIndependentUnitsCorrespondent.retrieveUnitFamilyIndependentMeasureUnit(measure);
+		else return MeasureDependentUnitsCorrespondent.retrieveUnitFamilyDependentMeasureUnit(this.unitFamily,measure);
+	}
+}

+ 9 - 0
src/components/page/WeatherInfoEnum.js

@@ -0,0 +1,9 @@
+export default {
+	UNKNOWN: 'unknown',
+	CLEAR : 'clear',
+	CLEAR_NIGHT: 'clear_night',
+	CLOUDS : 'clouds',
+	FEW_CLOUDS : 'few_clouds',
+	FEW_CLOUDS_NIGHT : 'few_clouds_night',
+	RAIN : 'rain'
+}

+ 17 - 0
src/main.js

@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import App from './App';
+import router from './router';
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-default/index.css';    // 默认主题
+import SIdentify from './components/page/Identify';    //自定义组件
+import "babel-polyfill";
+import axios from 'axios';
+Vue.use(axios);
+Vue.component("SIdentify",SIdentify);
+Vue.use(ElementUI);
+Vue.use(axios);
+
+new Vue({
+    router,
+    render: h => h(App)
+}).$mount('#app');

+ 56 - 0
src/router/index.js

@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import Router from 'vue-router';
+
+Vue.use(Router);
+
+export default new Router({
+    mode: 'history',
+    routes: [
+        {
+            path: '/',
+            component: resolve => require(['../components/page/ConfigurationScreen.vue'], resolve)
+        },
+        {
+            path: '/readme',
+            component: resolve => require(['../components/common/Home.vue'], resolve),
+            children:[
+                {
+                    path: '/',
+                    component: resolve => require(['../components/page/Readme.vue'], resolve)
+                },
+                {
+                    path: '/modifyUser',
+                    component: resolve => require(['../components/page/ModifyUser.vue'], resolve)
+                },
+                {
+                    path: '/modifyPassword',
+                    component: resolve => require(['../components/page/ModifyPassword.vue'], resolve)
+                },
+                {
+                    path: '/success',
+                    component: resolve => require(['../components/page/Success.vue'], resolve)
+                }
+            ]
+        },
+        {
+            path: '/register',
+            component: resolve => require(['../components/page/Register.vue'], resolve)
+        },
+        {
+            path: '/register-success',
+            component: resolve => require(['../components/page/RegisterSuccess.vue'], resolve)
+        },
+        {
+            path: '/login',
+            component: resolve => require(['../components/page/Login.vue'], resolve)
+        },
+        {
+            path: '/',
+            component: resolve => require(['../components/page/ConfigurationScreen.vue'], resolve)
+        },
+        {
+            path: '/weather',
+            component: resolve => require(['../components/page/Weather.vue'], resolve)
+        },
+    ]
+})

+ 6 - 0
src/utils/utils.js

@@ -0,0 +1,6 @@
+var utils = {
+    emailReg : /^[a-z0-9A-Z]+([-|_|\.]+[a-z0-9A-Z]+)*@([a-z0-9A-Z]+[-|\.])+[a-zA-Z]{2,5}$/,
+
+}
+
+export default utils

+ 0 - 0
static/.gitkeep


+ 77 - 0
static/css/color-dark.css

@@ -0,0 +1,77 @@
+.header{
+    background-color: #98B6CA;
+}
+
+.login-wrap{
+        position:absolute;
+        top:0;
+        left:0;
+        height:100%;
+        width:100%;
+        background-image:url("/static/img/bg.png"); 
+        background-position: center 0;
+        background-repeat: no-repeat;
+        background-attachment:fixed;
+        background-size: cover;
+        -webkit-background-size: cover;/* 兼容Webkit内核浏览器如Chrome和Safari */
+        -o-background-size: cover;/* 兼容Opera */
+        zoom: 1; 
+}
+.plugins-tips{
+    background: #eef1f6;
+}
+.plugins-tips a{
+    color: #20a0ff;
+}
+.el-upload--text em {
+    color: #20a0ff;
+}
+.pure-button{
+    background: #20a0ff;
+}
+    @font-face {
+		font-family: avenir;
+		font-weight: bold;
+		src: url('/fonts/AvenirNext-DemiBold.ttf');
+	}
+
+	@font-face {
+		font-family: avenir;
+		font-weight: normal;
+		src: url('/fonts/AvenirNext-Regular.ttf');
+	}
+
+	@font-face {
+		font-family: avenir;
+		font-weight: 500;
+		src: url('/fonts/AvenirNext-Medium.ttf');
+	}
+
+	html {
+		background-color: #FBF9EB;
+		font-family: avenir;
+	}
+
+	button {
+		font-family: inherit;
+		color: inherit;
+	}
+
+	input {
+		font-family: inherit;
+		font-weight: 500;
+	}
+
+	body {
+		margin: 0;
+        padding: 0;
+	}
+
+	.text-color-main {
+		color: #7DA5BF;
+	}
+
+	#app {
+		width: 100%;
+		height: 100%;
+	}

+ 167 - 0
static/css/datasource.css

@@ -0,0 +1,167 @@
+.vue-datasource *{
+    box-sizing: border-box;
+    font-size: 14px;
+}
+.vue-datasource .panel {
+    margin-bottom: 22px;
+    background-color: #fff;
+    border: 1px solid transparent;
+    border-radius: 4px;
+    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+.vue-datasource .panel-default {
+    border-color: #d3e0e9;
+}
+.vue-datasource .panel-heading {
+    padding: 10px 15px;
+    border-bottom: 1px solid transparent;
+    border-top-right-radius: 3px;
+    border-top-left-radius: 3px;
+}
+.vue-datasource .panel-default > .panel-heading {
+    height:56px;
+    color: #333333;
+    background-color: #fff;
+    border-color: #d3e0e9;
+}
+.vue-datasource .pull-left {
+    float: left !important;
+}
+.vue-datasource .pull-right {
+    float: right !important;
+}
+.vue-datasource .form-group {
+    margin-bottom: 15px;
+}
+.vue-datasource label {
+    display: inline-block;
+    max-width: 100%;
+    margin-bottom: 5px;
+    font-weight: bold;
+}
+.vue-datasource .form-control {
+    display: block;
+    width: 100%;
+    height: 36px;
+    padding: 6px 12px;
+    font-size: 14px;
+    line-height: 1.6;
+    color: #555555;
+    background-color: #fff;
+    background-image: none;
+    border: 1px solid #ccd0d2;
+    border-radius: 4px;
+    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+    -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+    transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+}
+.vue-datasource .btn {
+    display: inline-block;
+    margin-bottom: 0;
+    font-weight: normal;
+    text-align: center;
+    vertical-align: middle;
+    touch-action: manipulation;
+    cursor: pointer;
+    background-image: none;
+    border: 1px solid transparent;
+    white-space: nowrap;
+    padding: 6px 12px;
+    font-size: 14px;
+    line-height: 1.6;
+    border-radius: 4px;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+.vue-datasource .btn-primary {
+    color: #fff;
+    background-color: #3097D1;
+    border-color: #2a88bd;
+}
+.vue-datasource .table {
+    width: 100%;
+    max-width: 100%;
+    margin-bottom: 22px;
+    border-collapse: collapse;
+    border-spacing: 0;
+    text-align: center;
+}
+.vue-datasource .table > thead > tr > th {
+    vertical-align: bottom;
+    border-bottom: 2px solid #ddd;
+}
+.vue-datasource .table th ,.vue-datasource .table td {
+    padding: 8px;
+    line-height: 1.6;
+    vertical-align: top;
+    border-top: 1px solid #ddd;
+}
+.vue-datasource .table-striped > tbody > tr:nth-of-type(odd) {
+    background-color: #f9f9f9;
+}
+.vue-datasource .success th ,.vue-datasource .success td{
+    background-color: #dff0d8;
+}
+.vue-datasource .pagination {
+    display: inline-block;
+    padding-left: 0;
+    margin: 22px 0;
+    border-radius: 4px;
+}
+.vue-datasource .pagination > li {
+    display: inline;
+}
+.pagination > li > a,.pagination > li > span {
+    position: relative;
+    float: left;
+    padding: 6px 12px;
+    line-height: 1.6;
+    text-decoration: none;
+    color: #3097D1;
+    background-color: #fff;
+    border: 1px solid #ddd;
+    margin-left: -1px;
+}
+.pagination > .disabled > span, .pagination > .disabled > span:hover, .pagination > .disabled > span:focus, .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus {
+    color: #777777;
+    background-color: #fff;
+    border-color: #ddd;
+    cursor: not-allowed;
+}
+.pagination > .active > a, .pagination > .active > a:hover, .pagination > .active > a:focus, .pagination > .active > span, .pagination > .active > span:hover, .pagination > .active > span:focus {
+    z-index: 3;
+    color: #fff;
+    background-color: #3097D1;
+    border-color: #3097D1;
+    cursor: default;
+}
+.vue-datasource .pagination > li:first-child > a, .vue-datasource .pagination > li:first-child > span {
+    margin-left: 0;
+    border-bottom-left-radius: 4px;
+    border-top-left-radius: 4px;
+}
+.vue-datasource .text-center {
+    text-align: center;
+}
+
+
+
+
+@media (min-width: 768px){
+    .form-inline .form-group {
+        display: inline-block;
+        margin-bottom: 0;
+        vertical-align: middle;
+    }
+    .form-inline .control-label {
+        margin-bottom: 0;
+        vertical-align: middle;
+    }
+    .form-inline .form-control {
+        display: inline-block;
+        width: auto;
+        vertical-align: middle;
+    }
+}

+ 113 - 0
static/css/main.css

@@ -0,0 +1,113 @@
+*{margin:0;padding:0;}
+html,body,#app,.wrapper{
+    width:100%;
+    height:100%;
+    overflow: hidden;
+}
+body{
+    font-family:"Helvetica Neue",Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif;
+}
+a{text-decoration: none}
+.content{
+    background: none repeat scroll 0 0 #fff;
+    position: absolute;
+    left: 250px;
+    right: 0;
+    top: 70px;
+    bottom:0;
+    width: auto;
+    padding:40px;
+    box-sizing: border-box;
+    overflow-y: scroll;
+}
+.crumbs{
+    margin-bottom: 20px;
+}
+.pagination{
+    margin: 20px 0;
+    text-align: right;
+}
+.plugins-tips{
+    padding:20px 10px;
+    margin-bottom: 20px;
+}
+.el-button+.el-tooltip {
+    margin-left: 10px;
+}
+
+.el-table tr:hover{
+    background: #f6faff;
+}
+.mgb20{
+    margin-bottom: 20px;
+}
+
+.move-enter-active,.move-leave-active{
+    transition: opacity .5s;
+}
+.move-enter,.move-leave{
+    opacity: 0;
+}
+/*BaseForm*/
+.form-box{
+    width:600px;
+}
+.form-box .line{
+    text-align: center;
+}
+.el-time-panel__content::after, .el-time-panel__content::before {
+    margin-top: -7px;
+}
+/*Readme*/
+.ms-doc .el-checkbox__input.is-disabled+.el-checkbox__label{
+    color: #333;
+    cursor: pointer;
+}
+/*Upload*/
+.pure-button{
+    width:150px;
+    height:40px;
+    line-height: 40px;
+    text-align: center;
+    color: #fff;
+    border-radius: 3px;
+}
+.g-core-image-corp-container .info-aside{
+    height:45px;
+}
+.el-upload--text {
+    background-color: #fff;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    box-sizing: border-box;
+    width: 360px;
+    height: 180px;
+    text-align: center;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+}
+.el-upload--text .el-icon-upload {
+    font-size: 67px;
+    color: #97a8be;
+    margin: 40px 0 16px;
+    line-height: 50px;
+}
+.el-upload--text {
+    color: #97a8be;
+    font-size: 14px;
+    text-align: center;
+}
+.el-upload--text em {
+    font-style: normal;
+}
+/*VueEditor*/
+.ql-container{
+    min-height: 400px;
+}
+.ql-snow .ql-tooltip{
+    transform: translateX(117.5px) translateY(10px) !important;
+}
+.editor-btn{
+    margin-top: 20px;
+}

+ 27 - 0
static/css/theme-green/color-green.css

@@ -0,0 +1,27 @@
+.header{
+    background-color: #00d1b2;
+}
+.login-wrap{
+    background: rgba(56, 157, 170, 0.82);;
+}
+.plugins-tips{
+    background: #f2f2f2;
+}
+.plugins-tips a{
+    color: #00d1b2;
+}
+.el-upload--text em {
+    color: #00d1b2;
+}
+.pure-button{
+    background: #00d1b2;
+}
+.vue-datasource .btn-primary {
+    color: #fff;
+    background-color: #00d1b2 !important;
+    border-color: #00d1b2 !important;
+}
+.pagination > .active > a, .pagination > .active > a:hover, .pagination > .active > a:focus, .pagination > .active > span, .pagination > .active > span:hover, .pagination > .active > span:focus {
+    background-color: #00d1b2 !important;
+    border-color: #00d1b2 !important;
+}

BIN
static/css/theme-green/fonts/element-icons.ttf


BIN
static/css/theme-green/fonts/element-icons.woff


File diff suppressed because it is too large
+ 1 - 0
static/css/theme-green/index.css


+ 162 - 0
static/data.json

@@ -0,0 +1,162 @@
+{
+    "pagination": {
+        "total": 15,
+        "per_page": 15,
+        "current_page": 1,
+        "last_page": 1,
+        "from": 1,
+        "to": 15
+    },
+    "data": [
+        {
+            "id": 1,
+            "name": "Jaylen Schmidt",
+            "email": "aheaney@example.org",
+            "city": "Conroyburgh",
+            "company": "Kunde, Gerhold and Runte",
+            "job": "Soil Scientist",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 2,
+            "name": "Ms. Desiree Franecki III",
+            "email": "pweissnat@example.net",
+            "city": "New Mathew",
+            "company": "Davis Ltd",
+            "job": "Customer Service Representative",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 3,
+            "name": "Clyde Corwin",
+            "email": "rolfson.lexus@example.com",
+            "city": "East Ron",
+            "company": "Zieme and Sons",
+            "job": "Claims Taker",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 4,
+            "name": "Mr. Tyrese Kuphal",
+            "email": "libby.heaney@example.com",
+            "city": "Cristianland",
+            "company": "Abernathy LLC",
+            "job": "Occupational Health Safety Technician",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 5,
+            "name": "Ms. Amya West PhD",
+            "email": "uheller@example.org",
+            "city": "Treutelmouth",
+            "company": "Mraz-Effertz",
+            "job": "Hazardous Materials Removal Worker",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 6,
+            "name": "Murphy Stamm IV",
+            "email": "ckautzer@example.com",
+            "city": "Myleneshire",
+            "company": "Sporer-Wolf",
+            "job": "Pipelaying Fitter",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 7,
+            "name": "Elsa Jast",
+            "email": "kaitlyn.lang@example.net",
+            "city": "Mariahstad",
+            "company": "Hackett LLC",
+            "job": "Record Clerk",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 8,
+            "name": "Hardy Mosciski DVM",
+            "email": "soledad44@example.net",
+            "city": "Jasminborough",
+            "company": "Haley Ltd",
+            "job": "Kindergarten Teacher",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 9,
+            "name": "Demarcus Littel",
+            "email": "americo84@example.com",
+            "city": "New Lilaton",
+            "company": "Satterfield Group",
+            "job": "Plant Scientist",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 10,
+            "name": "Dr. Shad Gleichner",
+            "email": "eleanora23@example.com",
+            "city": "Lake Whitneyberg",
+            "company": "Fay Group",
+            "job": "Rotary Drill Operator",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 11,
+            "name": "Milford Mann",
+            "email": "shartmann@example.net",
+            "city": "Lake Austinport",
+            "company": "Sporer-Langosh",
+            "job": "Social and Human Service Assistant",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 12,
+            "name": "Prof. Mustafa Lindgren Sr.",
+            "email": "lizeth.morissette@example.net",
+            "city": "Roweborough",
+            "company": "Mitchell-Ratke",
+            "job": "Shoe Machine Operators",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 13,
+            "name": "Mrs. Brittany Bode Sr.",
+            "email": "wiegand.mozelle@example.org",
+            "city": "South Maxwellville",
+            "company": "Reilly Inc",
+            "job": "Bridge Tender OR Lock Tender",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 14,
+            "name": "Dariana Bauch",
+            "email": "dessie.schamberger@example.net",
+            "city": "East Linnie",
+            "company": "Wuckert PLC",
+            "job": "Elementary and Secondary School Administrators",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        },
+        {
+            "id": 15,
+            "name": "Jalon Renner",
+            "email": "lulu45@example.net",
+            "city": "New Rashad",
+            "company": "Muller-Kuhn",
+            "job": "Manufactured Building Installer",
+            "created_at": "2017-01-13 19:17:16",
+            "updated_at": "2017-01-13 19:17:16"
+        }
+    ]
+}

+ 71 - 0
static/datasource.json

@@ -0,0 +1,71 @@
+{
+    "data": [{
+            "id": 1,
+            "name": "段娜",
+            "email": "g.rgiuory@kctbut.mw",
+            "ip": "68.28.4.232"
+        },
+        {
+            "id": 2,
+            "name": "蔡洋",
+            "email": "y.mwjjoje@lpkshev.tg",
+            "ip": "22.126.12.189"
+        },
+        {
+            "id": 3,
+            "name": "陈敏",
+            "email": "e.voaiiuo@mvng.sn",
+            "ip": "227.89.13.37"
+        },
+        {
+            "id": 4,
+            "name": "朱平",
+            "email": "e.lduuf@nkfypn.az",
+            "ip": "9.39.240.243"
+        },
+        {
+            "id": 5,
+            "name": "侯平",
+            "email": "t.czqjyndts@jmwenklns.md",
+            "ip": "178.162.29.113"
+        },
+        {
+            "id": 6,
+            "name": "常超",
+            "email": "d.dhysgem@uxpcutmlk.tt",
+            "ip": "192.50.103.170"
+        },
+        {
+            "id": 7,
+            "name": "许平",
+            "email": "g.fiqdonvbc@wanepptw.tv",
+            "ip": "73.20.99.60"
+        },
+        {
+            "id": 8,
+            "name": "毛超",
+            "email": "w.unyyejh@qus.gt",
+            "ip": "10.88.135.123"
+        },
+        {
+            "id": 9,
+            "name": "周磊",
+            "email": "e.qbejguqqg@ejpxhltoak.gw",
+            "ip": "244.221.237.210"
+        },
+        {
+            "id": 10,
+            "name": "胡秀英",
+            "email": "s.dszo@uxaojtj.sy",
+            "ip": "86.199.17.210"
+        }
+    ],
+    "pagination": {
+        "total": 15,
+        "per_page": 15,
+        "current_page": 1,
+        "last_page": 1,
+        "from": 1,
+        "to": 15
+    }
+}

BIN
static/fonts/AvenirNext-DemiBold.ttf


BIN
static/fonts/AvenirNext-Medium.ttf


BIN
static/fonts/AvenirNext-Regular.ttf


BIN
static/img/bg.png


File diff suppressed because it is too large
+ 1288 - 0
static/img/bg.svg


File diff suppressed because it is too large
+ 58 - 0
static/img/clear.svg


File diff suppressed because it is too large
+ 14 - 0
static/img/clear_night.svg


File diff suppressed because it is too large
+ 6 - 0
static/img/clouds.svg


File diff suppressed because it is too large
+ 30 - 0
static/img/few_clouds.svg


File diff suppressed because it is too large
+ 2 - 0
static/img/few_clouds_night.svg


File diff suppressed because it is too large
+ 10 - 0
static/img/home.svg


BIN
static/img/img.jpg


BIN
static/img/logo.png


+ 175 - 0
static/img/logo.svg

@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="93px" height="170px" viewBox="0 0 93 170" enable-background="new 0 0 93 170" xml:space="preserve">  <image id="image0" width="93" height="170" x="0" y="0"
+    href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAF0AAACqCAYAAADP5kNiAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
+AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAA
+CXBIWXMAAAsTAAALEwEAmpwYAAAlAElEQVR42u2dd3gc5bn2f+/M7myRVr13W5I7trEBmxobQgsl
+JISSQz+EEgiHEgglB+ITvlCTEAjfFxIgB07ozZAQioPLiamGAAHjJku2JEtW79LuzuzM+/0xu2vJ
+lmTJXmllovu6xvLutH3veeZ5y9NgaDjCf73A1cBqoA2wADm5DdisMDerw1x5d+NwRIgcfCywfQI0
+6kDbqsPcDUq8GILwEPBd4OXwdzqgACpCiMFP+1eGBCklYGJLvRbecSbwCrs4BfZkTw2fOANYH/6s
+A5pQHDgT07DMEEgZ71ZOLAiBojowetqQVgjCnGFzORvYzC5u94Aa/vsq9msSBKRQtXi/rgfM1o+r
+YPjv8t24HSDpkSdRDmzAfiWkUDUhTR2hKCw+9yZSc4pBTKqXAZCS9obtfPjsfUgJQtWQpi6x+TWA
+WcBWwhz3V/IRJg/HJtxEKKridGGaOsdf9UtyyuZihgxbrU8iCiklGUXT8KXlsOLhG1CcbkwrJJCW
+CTixOd1KmOPBSC+MXEvVvJiBbooWfJP0gjKMoB9pWZOSvjukxDJDpBeUUXTwcdR8thLVlYgZ7Il0
+fhFO9yA9AmfkUkKxd3uS0hCqirQshKLEu4kTD2EhFKqKJznd/r/NXYR0rf/hgzG4x9BESivezTpg
+MARXAzgdkdiKyXH5iDESriZ1RRwwSXocMKIFGYkMD/3DqkkMeeC+rxDsfm7/z3u77v7uH+15g/3W
+8F+5Z5e4B0ZEuhDKwFHLpIofiIgsKgpC7F15DEt6pCc2QzpGoA/LMvd6wX9lKIqKGdKB4Ud8w5Ju
+BroAqPrwLeo2fjKCF+dfGwIIdrYAu7gbDCNQLwIrFMDfuiPebTqAIGAYER0B6RIhFByaO94tOSAQ
+0gN7nUyOqCNVHQ58SUkwqWD2AkFXR4iQoQ971F5Jd2puDD1AW3NDvFt0wCDC2VAYlvTUjGzaWxr5
+/qU/4rJrb8Y0Q4zVokAsh/jjDRn+V1UdPPrgvTz7+MNR7gbDsKQ7nfaCY0paOiWlBZgRK9/kOH0g
+wlpXddhcwS7uBsNexun21cxQiGAATNPuIESkcx5sViaG+TzYeYOdP9S5g12XIa4ph7jWYNcbyXH9
+793/d4pdE3U1pGCGQgO4GzXp0esLgaKAlMPMSkf6eai3ROz9XCnlrmm23P0QMdCiNdz9BrvX3o4b
+5nPktorCiKxqo3KGkVLGxVRnWRZCCFSHQFVtARi4H0wTzJBESokSB0OLLdkj42ZUpI+3Mo9Yqtxu
+BSmhq7OPlqYG2lubsSxb1SmKQmp6JhlZOfiSvShCoOv2gxpf8kfOzShJHz9YloXmUrBM2LJxCx/8
+fSVvv/Yi695dPejxi44+jm+ecgZHLDmB0mnTcDgV9OB4Ez8yTEjSpWXh9ijU1zbwwpN/4OF7fxbd
+50tOxZecGlVzUkq6O9v5aO1KPlq7EoAf3fJzzrrwMvILcwj6J55dd8KRLqXE5Vb4xwcfc84JhwGQ
+VzQVIaCzox09GKCrpRav2yayL2BhWA4Sk9NISk4BJA/fcwcP33MHz634iEMWH0YwGJ++aChMKBGw
+LAuXW7B21eoo4cWl06ivqaKuuopMn0F2qqCnz6CpLUhTW5CePvu7LJ9OfU0V9TXbKCqdBsC5Jyxi
+7cpVuFzCdh2ZIJgwkm6FVcrn6z7lkjOOJTO3ADNkUF25hbQkJ16PyrYdPQCUFSdzzGHFAPzvR9VU
+1nQCkJflJhA0qancQlpmNqrDySXfOY6XVv2D+YcuIOCfGDp+QpAupUTTFJp2tnLh6cfY31kWbc2N
+5GV5qG/y09ZlcM/Nx3HKsdPJz07Cl+gCoLsnSF1DF2+s3szN99o6PT/LQ11TIxnZeQD8+5nH89a6
+LWRkpmMY8Vc18X/sEQj4y0tP09fbS0nZdFoa68kPE56f5eXzv17JzT9aypw5+aSmJ+BwqDgcKqnp
+Ccw5KJ+f/Ggpn7/xQ/KzvdQ1+cnPctPSWE9J2Qy62tv4y4tPT5jli7iTLi0LTRPsqN7B3bddS2pG
+Ntu3biY50Uldk5+kRCfvv3IF8xYWIY0QVsBAGqY9O7Uk0jDD34WYt6CQ91+5gqQEJ3VNAZITnWzf
+uonUjCzuvu1a6qp3oGnx1+9xJx3safSnH70HQKLPhwIk++wFo789dTFFZZmEeoLh5Qh7ui/C5/X/
+LtQTpKg0k789dRGEryGARF8SYN9jIgxi4kq6lBJFVQgETD5cuwqAQCBAfraHmp19/OTywzlsfiHS
+b6COoANUFQX8BocdXMRNlx9Ozc4+8rM9BAP22vaH764mELBQVGXYBamxRtwlXVWho62NlX+1fec7
+O3twu+3+/fTjZ4KmYllyRBIqBFiWBE21zwU8bgcdXfaoZ9Wbr9HR3oqq7v1aY4n4ki4lQoHenm7a
+W5tJz8rF1Htp6/ADUJiXDJYcff9nSYpykwFo7wxgBntJz8qhtamB3u5ue8HsX1nSw5N5+/9CkOBW
+aO0MsXB2JqnJHjCtUelhIQSYktRkDwtmZ9DSYZDgViDqBBR/O2/cSYeBNEQGFl09QUKhfQlAkCAg
+ZFp0dgej15wA/WcUcSddAqpiK1nD0OkNQmGOm4rqLlra+0AVo9IE0r4gLW19VNZ0U5TjoTcIhm4/
+AEVR4y7r8SVdCCwTklNSmbtwMZ1tLST4knBp9nBx09bmfQu1EbCpsgkATVNJ8CXR2d7K3EMOJzk1
+DcskriE8cSVdYFt8fMnJLD7GDjBOTPTS1WNL5R+eWUegK4DiUOxRyV5gWRLFoRDoCvDI0+sA6OrR
+SUy0o8YXH3MsvqQkTDO+6ib+km5ZaBosOtomXdNcNLXplBYm8vrqSl5+cz1ojl3DwSEQHVZqDl56
+Yz1vrKmitNBHU5uOptnrNIuPOhZNsxfX/mUlHezRRigEBx18KOUzD6KmagtubyKVtT0U5ng4//rl
+rFixAeF2ojjtMbtl2bZQKWX0s+JUEW4nK1Zs4IIbllOY46Gythu3N5Gaqi1Mmz2Pgw4+hFBoZMbj
+rz3phmGRlpHE9bffBUB+UQkALe0G6ckOTrzoKR578gO6Ov0obieKx4nQHAjNgeJxoriddHX6efx/
+PuTEi54iI8VBS7sx4FrX/fQXpGUkYRhW3EmfEEu7QggMHY4+7mTOvvhKXnjiEaZOm0XVlg1IqVKU
+4+GyW1/noSc+4IZLj2RmeRY5mT4AGpq72VjRxAOPv8cXm1spzPHQ3KYT0E2mTptF5ab1nHvJDzn6
+uJPQ9fhLOUwg0k3TwuVSufbWO1n37hqqtmxgSvlMtlVspKbBT2mhjy83t3LJT/485HVKC31U1nYD
+MKV8pn2Nshlcc8vPcblUDH1i2EsnBOlgu1LoukV2bgb/vfwdrjrvdDZ+8Sml02fT2tJEZW0LiW5B
+dqYXIQRtHfYiVlqKGyktGpv9VNb2kJKWSXpWNpWb1jN9znweefZ1snMz7IWuCUA4TACdPuDHKAqB
+gEVBcT5/fOVvXPKjG6nc/BUdrc3kFZWQklNKiz+Jqp0mgZCTQMhJ1U6TFn8yKTml5BdPoaOtmcpN
+67n46ht5YvlKCorzJxThMIEkPQJFUQgGLNIz0rhp2X0sOf5UnvjdA6x+67UBx/UZu/7f2eInHHXC
+0pO+zUU/vI7DjvwGDocgOMEIhwlIOtjER/TvUcd+g3mHLGbr5q/4bN37fPnZJ7y/ZgUtjTsByMjO
+5YglJzB3waHMXbiIaTPn4EtyEwyCoU88wmGCkg5EOzy/38LtcTH/0AXMW7iAvj4df19f1AghhMDj
+9eL1aigKGIZ9jrJ7GOYEwoQlPQJFUTBNSShkW/E1TcPt1ga4OlsW6LqMOrhOROnujwlPOtjSHHWj
+syQha/hjJjoOCNIHQAwefhNZFhj+1InxYA480neDtCwk4HAoqA4R9hDY7Rhpb6EQmCHL9iSIowo6
+YEmPpLXS3AoC6O4O0NbcxM66Wjrb22ypF3Zeg9S0dHLyC0nPyCLBpyHDfYBtox1/8g9I0iO+69KC
+7Vu38dG7a1j15p9Z9earw5530hnnsPSk0zj08GMoKC4EBHochpUHFOmR0Ynbo1BXs5NXn/sfHrjz
+luj+lPQsvAkJqLv5WIRCIXp7enjr1ed569XnAbj1rgc55bvfJzc/k0Bg1/BzPBBX0u2Or1/gViQB
+2SCNt2OJBKoKa1a8ww/OPB6AjOw8PN4E2pob6epooy+go2ouHE4NJIQMnZBuu2EkJqWQlpFFZ0c7
+d992LXffdi1P/XUti446CsMYv5iqcVdotuHBHvM5HALNpdibW8Gp2S5y0rIG+BvanmC2gfqZPz7K
+D848HpfHS+HUctpaGqndVoGakIovqxC9twN/eyPdTbV0N9fi72jE6OskKbsI4fZRU7WF3u5Oikun
+A3D+KUez/LlnUBTse4+DP8y4SrplWaiqgqbZ1qLOji66OzuiDXVqGqnpGXi9TiwJRtDuLCO+i396
+9Lfcdet/MGXaLBrqaqmtqsCTmo1Tc9PVWA1AcnYhMw47jszCUqSUNFVXsP7dN+ior7L3504h2NdN
+deVmEpNTSc/I4idXnIeh65x1wcVIa+wlfdxIl5aF263g94f4/ONPeH/N31i76m0+X/fegONOO/sC
+jjnuZBYdvZS8ghx0HZxO+OsrL3PXrf9B+ay5VGz4wiYwZwqdDdvwAydfdjvzl5xGZmEZbk8CTped
+ClEP6gT6ummsruDTlct558n7AEjKLqarsRp/bw+lM+bw02suITM7l2NPPnHMgwfGhfRIHNGWjVv4
+/QN38dpzT0b3JadlhNdJBG3NjfzlhT/xlxf+BMD9f3iW0886l80bNnHdJd8jt7CE6soKAFLzy2iv
+28q0Rcdzzo2/omj6QagO27tAWruclpyahuZOJzUznbJ5iznsxLN58udXULfpH6TkTaWjvoqdO2pJ
+Tsvk8rNPYsU/qphSPmVMVyfHnHQpJS6X4N1Va7j4jKUAFJVOw9AN+no66elsx+FOxDKCaJpGclom
+Hm8CrS1N3HT599m6+Ssa6uwEP7oeRA/6Sckrpb1uK8ecfTVnXns3vjQfekBihuQenbFlSbAkpmF/
+V37wQn78+7d59t7r+fiNP5GcO4XOndvILSihs62ZJx/5Dbfd9RubcCnHxGtgTEmPxBF9+Pf3uPiM
+paRmZONyuamp3IKqefFl5OJRvfS07ECoTlJyS+jt7aa5YQuqw0nR1Gn8/lf/B4CU9ExaG3eSmJFP
+R30lh5x8HmfdcD8en4dgn4miqoMSZJO/K89+oM8kOTOd8257mJ7OVja+9wYJabns3LGdoqnTePrR
+h/j22RewcPEh+P0SZQxIHzPFZVkWLpfCjup6zj/lKFyeBAAa6qpxJ2dimQYd9ZX0tOwgvWg60jRo
+31FBX3sDSTklmCGDmqotZOcVkpSaQUdrM05PEj0tdQB877p78CZ5MAJhwkfaYFVF94dISk/i3255
+EAA9GEB1ujF0OznOitdfRtfZJe0HCum2sRle+tNjAGTn5tPe0ognNZtAZzPSNLjkF09x518quP2Z
+j7jvnZ3c9MS7LDrtYroatpOcUwJAY30tXe22WSgxLQuAS+95jqyiAvSANSrCdxHvQPdb5JWWcd4d
+j2L0tuPLzKO1qR6ny8NjD95D4856HI6x8fGNuXqRUmKZJm6vg+qqGh6+92ekZ+VSV1OFy5eOv72R
+zOIZXPPbP1NQXm53eiYkpiSTkZ/DlDmHkV8+h1d+fWN0hAEgFJX2ukoAZi46FisGTkPSgoOOPAkA
+f08Xuq5TVDqNmsotbK/cQmFJHoGAicMRW5piKun2OFzg9jpQBHzxyYcAJPh8mKEQTrcHgCvue5ai
+6eUE+0yMoGUbKQxJsC+E5nJy/HnXceSZV9DVWE1CRj4AvswCQHLKFctITs+MGjX2FUJRMEOQmpXP
+0n+7jmBXC560PEzDNr5WbPoKywSv14FQiE7oYoGYPcLIOLy31+CL9z9kzYo3eHO5vc6xo6oiKrUn
+/vttFM+cT6BvT9UgVAeGHsLldfCN713Oey//HmfYD9ER/ptXOsseGob2f2QhpYVTUyiYNhcAjzeB
+upoKVIeTX/7sJhp21LLkxFOZt3AR3gQnwcDQfjO75wAaDjGR9Mg4fPNXm7n5qgu54NRjePyhe6iv
+3YbqcGJJiaLaz7f84CNRHEP/OqGomCHIyCshs2QmHfVVqE43obB/eWp2Qez0bDhFS1qOncjfDOfL
+skyTYMDP47+9lwtOPZpbrr6IzRs243KHA8QG6Vwj30RGO8NlJd5v0qPj8NVrOOXwGby1/DlKymaQ
+U1CMprkwQwaq5o2OXhKS04cVUIFAWuDyJpKeVwKAlpCEJ9EOS9Q8CTF1cxbCvheAO8GHqnmQ0l6u
+yMkvoqRsBm+88iynLJ7Bu6vX2HkGhriOxJ5LAGhODXbVNhp90vqhEEmg8NG773Pxt5eSlplDdl4R
+27duoqW5hYTMAjypOZh6H42V9tQ9Nacg7JQ/JOtIS6K5NKYvtEPWVadGY+WXAKRk5sbOqV/YnXhE
+0hu3foGp+/Gk5pCUM5WW1ja2b91Edn4RaZk5XPztpax77wNcbrGHjpeWharCrLkLAGhrbZZAJIPo
+qv7k91eqKnY1qiXhzUpITFL7ero55PBjOPwbx9LfrVuGDQn1tTs57cjZqA4nbo+XlsY6PMmZ6P5u
+Al2thAI95E1fQMH0+Vx2z7MUlM8gZMhhsy9LQFEF+eVzcbi8NO+oouzgo7j8vufJKZ6y351olHMh
+sExJYkoSs488hdaGHWjeJNp3VBDobsMK6XiSM+lo3oknIZFAoI+Xn3qMM8+/kpQ0n2366/c7JIIp
+pdPJzMmjsmKj1HV9J9K60bKs5dgCbsJ+dKRC2Grg1fA6Sn7xFGoqt+BJzcbfbucjvPTuZ5k6dxGJ
+Kek4XR40txNzBEM9IQRmSJKQlMTpV97OCRfegFNz4XQ7MPXYrnnb94LSeYdyzUOvYQT99HS0sm39
+Oh67+Vz8nc14U7Npa26guHQ61ZWbee35J7ni+psHvG2RB6i5nNal11ypnH72+VsOKfbNAUwppRBC
+RF+NfSLdzlohqN1eywN33kpKehZ11VW4fGn42xvJKJrGtQ+/Tn6/cbiUEDKsEeUX70+8EAJPQoJ9
+vj7y80eLkGHhcDpxak4SU5LIK51C8cyFPHDVybTt2IrLl0ZddRXJaRn8+ue3cOr3zqOwuAC9nxAI
+IbAsKU1T4EtK1AFzyZIlds2oftinFthGBdj0VXiJNTUtPA63O8sr7nuOwt3G4fba0ehuF2nMvp4/
+unvZydlMU2IELQK9JoXTy/jhL18EwOn2EgoZpKZlALD5q3+iqHvmX7TdPCBk2PkU16xZE9r9vRxV
+KyInK2En/o1ffgaAv7ebxMxCeppr+eaFN1E862ACfRJFVcPZ8/dPHYynr4oQAqEoKKpKoE9SPGs+
+37zwJnqad5CYWUhfr+3/vvGLzzAGCzKQkWVl+2EsW7ZMQYj9GL0IosudlgUtTXbSY8MIRScxZQcf
+ierg61HJUUpUB5TNPwIAp+bCMOxRYHNTQ3hgMZB0icTtBo9XMQGWLVtmnXXWWQNmgfs1Ix2gy8Ik
++9KyJkR6j1hBCLtNEFarezGeOzWhfPbxJt5fvWIK8OuUnJyHXnzxxe3YAm7BfpI+qDB/HSR8H9ou
+LQvNrfDPTz4V3zt2oQSSNJf7+o6GhnOABUAjYeJj3jONZWcXL+y1TRGVa8KKv7wCIMpnzZV6MOAH
+8oDzwkeqMAbr6UF/z9dK2KWEYF/3yE8YgWodHen9yBQCPB5v9D6RbM91W9d/7Uivq/wq3PxdyX48
+Xu+uvkuI6DD6hFO/CyArNnwhNJfbA9QDT4ePNGFfJD0cWu50wvTZ9pKoN9FHZ71tbHj1wZvpaGrG
+6VIO3HrU4fqiTpdCR2Mzyx+4EYDO+mq8CXb86vTZc3E6GZBoWQ9K5i5YIP/6wSZx210PdenBwK9T
+cnOPoJ8+HzXpA+gTMGPOPIBw0SkTX1YRIUPnrSfuJ+g3cHkcUSnY+2YN89ka5bmj3T9wQwhcHgdB
+v8FbT9yPadptAzNaYGvmnPl7qBI75F5a8w+dzg/+45ptwI87du6sDg8Z928ZIBLhPLV8Bt8+5yJe
+e/5JsvMKaayvISm7iHeevJ9gXw8nXnQj6bnFOF2qnVho2Iz+YjdLQCQj/G77BpWGUe4fpsqBtMAI
+mtRXVfP2E79k7Uu/Iym7iK7GGrLziqirruKMcy9mStn0QSdHAkEgAIE+SwV7crRs2bIBywD7TLpp
+WngSHJx/2TW89vyTeBLsNemuxhqSc0pY++LvWPvi7zjiO5dRUH4QqtPF2Jh5YwmBaQTZUfEF7y+3
+DerJOSV0NmwHwJNgL3Ocd9k1eBIcg3uCCcJJ9e2HsWzZMgspRf9Z6T6P0+14T8m8QxZyx/2/4+c3
+/ZDpcw6mcvNXdDZsJzEjHyMY4P3lj8abyX2Cy5eO0+Wms2E7qlOjbPpsNq//jDvu/x3zFi4gGBi8
+IoGtogQOp72quGTJEsdqMPu/D/ttIzVNOPuiy2lva+G3d99ObuEUFEVQV12FUDUS0vNQndqEiPUZ
+CaSUhAwdf0cLwe5W8oumIoHN6z/jmlvu5OyLLsM0hz5XVYVQVWjr6tEAx5o1a+w6Rf2w38sAliVx
+OBSuvOGn5BeWcMtVFwDg9SWTmpZxYC4JuD3IxELa21qoq7G9fe/5f//DaWedj6oKTHPPNf2IO3cw
+aChPP/4Yf/r9Q9O8vpQaI9D7X0KI3xOrZQDYpd8VReHM889n3iGLef3lZ/i/9/6Mvu7OeNO33/jR
+zf/FKWd+n7Lp5XagmDmEEUVKHA7BX196mTtvugp3gk8EertzgUeADcBabL5DMXHBEMLOsaUHoXxG
+GdfccgfnXnwF1VVbaairxQpHwB0IENj9VU5+IcVTy8jMycahQjA4fIiMUBRMEzaEl7tT0zLEzt7u
+ALaddCk26QJi6PcS+TGBgG03zM7LJq8ge8Tnx7sU2mAwLTB02xFqb27T4WC+aL4wwy4aGOE3djp9
+MER+nBG00PfzWoOhf1Gw4YqEjebNGspRKBJvOpJBQOSIyBK3GPySwBi6SgtFib3kSjmA4JEU4xoJ
+YRGTm4LY59dtsCpsQ+GACWmUlp2CW1VGJsURI5dpDq2LI9F0DkckgMCuzzfW4ewHBOlSSjSXQl+f
+jh4MjpgQIRR8Sd5wSMxA30cpJU6nHXDWHU717XK78XodhEJgGAdw+Mv+ImwCY/3n/+T+ZT+hoW4H
+DqcT0xy6snskhing7+O6n/6Ck884a1DC63fU8/hv7+eDv69EVVXmzD+EY47/FouPWkp6VgrBwNjE
+lU5o0qWUOJyClsZWvvON+UDYyWkUy8U3XHo2OXnvc9hRh0fXShRFoAdNfvOL/+TVZ/8bp8uNEQyw
+ef3nvPzUYxSUlPKrPzzDgkWHDfBriRUmtG1NSolDtS3vADPmLkBze0lMSiHBlzz0lpSC25tI2cyD
+AKiq2EQ48igceQ19fb188L/vUDi1HJfLjcOpkZ6Vw5Rps9ixvZJzTljE31euRHOJvRbq/lqRLoTA
+tOys0wCbvviUjMxsHE4NzeUeetNceLwJhMIFWbPzCuyHSGTpAtweDwcdfCi1VRVkZOeSlVtAX28v
+27ZsICu3gPSsXC797jep2FhhFy6ciEEBY0W6oUtyC/L4wwtvc/nZJ0bXQkaC9pZGrr/9bhYduWRA
+9lHLtPB4nFz/n7+gYuN6tldsjJ7jS0mjaecOcgtKAHjq0Yf46d0PxbRTndCkwy4HzyUnnMC7mxrp
+6+tF2c06P/jkSKIoKjl5BaiOXX6RYM8hdF1SPmsGL638mLa2Fmq3V/HrO29lw+efkJSazs4d28nO
+K+SZxx7moiuvZ+q0qTEL6J3wpEdgGJKsnCxG5eEh7ex15iCu1ZG3KCklieS0JGbMnkpeQRHfWjQd
+wzDQ3B6cmh3qXl1VQdmMqTFry4TW6XuQZEj0gIUekOhBa9cWsAZ+jnynS7tExlCLVLZNEz1o0d1l
+MbV8Gj+49lb8PV0kp6ZHzwsGgzFty5hI+kiSmO3nHfaclg7hbSYHO3aQ4xRVxbKIOogOLLUW27aM
+SRypqgqc2kRbMxzuN9t2zQ/+932eeexh0rNyaW9pwlVgl9/0JSXHlPeYkh6ZzPT1BPj8ky8wQyF7
+MjOuFI7qB4cjtCSbv/onP7/pKgB6u7swzRDdXbYRZmrZDMwYlu2JXRxpWMJ7uvq49/Yf88KTj4w7
+h/sLp8ttZ7b291JSPpPtFRu54Y57yM7LwdBjl9kupqQ7NcGn697jhScfYebchXR1dsSLv1FBhCP6
+InOAkvIZ0bH7d75/SXjZJnZmlpjr9N7uruj/+3q6UVQ1rhUR94qwFcSyLPKKpqJpGtsrNgHw2tov
+yc3Pinn+9diZ67ADuiKudhu/+Me4chcLdLTaBanOuvByrrj+NopLi8ckw1HsSA/P8qZOK+PlVZ+y
+/LknwgaBie9DaqsPwdTyGcw/9HBmzJ6Ly62OWUqpmKoXIQQhXTJ34cHMnn8wE6hq/MjIcIKqgK5D
+MHggGTGEQA9OjCoro4WhS/RxyME+JjPSCOEjzjsmI//0D/kOr5cMss++CdEJi7Tn+ojBzPmR86PX
+YshByHil+h6Txykt2x9cIIetXG5XRLdnHVG+osOzXZ+jntORfYI9Zoii/zHRY9nzILHr98ULY7IM
+oLmUaM5yRbV92Qc7TlEETqcIO2T2Z2l3xobbN9R3Q+9XNIWQESkyOP7LFTFfBtA0waavNvLm8ufR
+9SBLTjiVQ484coCna4Rwy7J467XX+XTde2iaa5+kr/8Ze6VeCHQ9yIJFR7L0xNPCv2H8iY9pOimn
+S2F71XZOO2JW9PvHHryHZ9/+kEMOXzRgCOZwwJ9feJEfX3buuDYY4I+/hV899jynn3U2+li4oe0F
+sSMduxz9hn9+CsDMeQsxdJ2tG7/kw7+vYuHiReGO0ZbyQMDkzdfsZAeFU8rw+/1jLnFSSjweD7Xb
+tvLWay9wwmln4nCo4z6PiLlOd0fDHAWay07sk5CYuFv4n+3z6EuyU0R5ExKxrLFLKxKBlBbecJhO
+oi8ZRVHiMnGLqdeuocOCRUdy8nfO5c3lz0X3ffNbZ9DfiyGSFenfLr2a5c/8N5vXfz7uDT/v0qvR
+tPjMKWJKumlKklN83PmbRznn4isJGToz5swnOzdzYDIaRcEw7Hilv326jW2VW+ySClGpGwvxs51D
+TdNkSuk0iktLYrpcOxrEfBnAMCSJvkSOPu4bgF3mZnAvKdswXFJaQum0knEzdESKiY+F59ZIEXOd
+HpH4UN8ub9nhDMN6cPyjNL6WdY5GM50eEz/2CY4Db1Xqa4BJ0uOASdLjgEnS44BJ0uOASdLjgEnS
+44BJ0uOASdLjgEnS44BJ0uOASdLjgEnS44BJ0uOASdLjgLiHNO5RrKm/91b/7Dd7K5k1ZJLNwa41
+Pu5zQ2GUpMc26VMkZEZ1jC8B5nAJ0Pa9NSPmZkSk9ys0H7ufGA6X6enqo6W5cY8o6Fg2t/9+S1pk
+ZGaTmOS1DdMxlviR8DQySZcy7JsoR+6JOwwsy0LTVOqqd/Cf1/2A91a9HdOG7w1HHXsSdz74GHkF
++ei6OWq36D1S9YY9gy05sgiIYUmPSIHDoeLWIKTGyp5pJzx+6tGHeW/V25ROn01LUyOqY/QFXUcD
+M2SSkZ3Du6ve4ulHH+bWX9yNqsTmnhKBQ7W56s/dYBiW9L6AHZ69rbaR1R9uwYp4gQ51vZGoNQlC
+EYQMg48/+RiHw0ldfT2mHttQ8KFQV1eHw+Fk3bp1vLP2SxxOZzjV1KgYHjQrtaKqbKttHMDdYBie
+dL/tXflldRPG2vXD+pqPChIcDgdWchGhkIEZCCANf6z5HRQhSyJDBlZKEa+sq7RzwsRIrQtFZVN1
+0wDuBsOwpCuKwAJ8bid5SV6kFZH0IX7lSDtwKRGKiu+IkzGDfrau+xvO1Gy7ssAYQRAuMtvbRdlR
+pzPjiG/hTXDbbRptJzW4UkdRVOrczgHcjZr0yGtjWZKQZdmvYf8d+wPTQPP4mH/SBcxeemYs+R0G
+9sDd6fKGXfuMMOGxGZsJLCwrGpMzJOI3ORICaZdsRAsXiB0vSMuK3jseiO+MNFIa2DIH75wGm5EO
+hlHvF3EjHOJN+gAS9vxq0P8Pcfp+7R9nTC54xQGTpMcBIyJdTtw0ORMOI+FqMNL30IBfxyKAY4Uh
+uNpr0vrIVEpIy56s+LtakaY9iZD9S6pPwoa0w2ikaeLvbLW/srmLEDVgetqf9Mh7URv+K0y9D9Wd
+RM2nK5lxxCnklM3FDBkHTPmc8YKUEtXhoGHrl9R8thLVnYQZ7IFdpEc47V+rDLBrZZpAOXa1Egcg
+haoJaeoIAYu//xNSc0omJX13SEl7w3Y+fOY+e0qgakhTj8wODGAWsJVdHA9AZJ3zVeynEsQmPmLH
+mNz2svXjKhj+u3w3bvfoNCNPYjrwVfizDmhCceBMTDtwy12OJYRAUR0YPW0RXa4DGhACZgNb6Cfl
+g+kJR/jg7wCvhL/TsUc6qq3QJ9XLQMhINRMTu2qXFt7xXWxJj3A6LCId7LHANibAa3uAbdXYBaX6
+czkiRA72AFcDq4C28JOMd6Mm2maFuVkFXAV4hyP8/wP/4YTBA8R+EQAAACV0RVh0ZGF0ZTpjcmVh
+dGUAMjAyMC0wNC0yMVQxNToxMToxMSswMDowMFsRh/cAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAt
+MDQtMjFUMTU6MTE6MTErMDA6MDAqTD9LAAAAAElFTkSuQmCC" />
+</svg>

File diff suppressed because it is too large
+ 2 - 0
static/img/rain.svg


File diff suppressed because it is too large
+ 2 - 0
static/img/refresh.svg


File diff suppressed because it is too large
+ 6 - 0
static/img/send.svg


File diff suppressed because it is too large
+ 6 - 0
static/js/vendor.dll.js


+ 43 - 0
static/vuetable.json

@@ -0,0 +1,43 @@
+{
+    "list": [{
+        "date": "1997-11-11",
+        "name": "林丽",
+        "address": "吉林省 辽源市 龙山区"
+    }, {
+        "date": "1987-09-24",
+        "name": "文敏",
+        "address": "江西省 萍乡市 芦溪县"
+    }, {
+        "date": "1996-08-08",
+        "name": "杨秀兰",
+        "address": "黑龙江省 黑河市 五大连池市"
+    }, {
+        "date": "1978-06-18",
+        "name": "魏强",
+        "address": "广东省 韶关市 始兴县"
+    }, {
+        "date": "1977-07-09",
+        "name": "石秀兰",
+        "address": "江苏省 宿迁市 宿豫区"
+    }, {
+        "date": "1994-09-20",
+        "name": "朱洋",
+        "address": "海外 海外 -"
+    }, {
+        "date": "1980-01-22",
+        "name": "傅敏",
+        "address": "海外 海外 -"
+    }, {
+        "date": "1985-10-10",
+        "name": "毛明",
+        "address": "内蒙古自治区 包头市 九原区"
+    }, {
+        "date": "1975-09-08",
+        "name": "何静",
+        "address": "西藏自治区 阿里地区 普兰县"
+    }, {
+        "date": "1970-06-07",
+        "name": "郭秀英",
+        "address": "四川省 巴中市 恩阳区"
+    }]
+}