Quellcode durchsuchen

feat:增加看展小程序

徐志豪 vor 5 Jahren
Commit
a9ae0de5b3
100 geänderte Dateien mit 5080 neuen und 0 gelöschten Zeilen
  1. 14 0
      admin/.editorconfig
  2. 15 0
      admin/.env.development
  3. 6 0
      admin/.env.production
  4. 7 0
      admin/.env.staging
  5. 4 0
      admin/.eslintignore
  6. 1 0
      admin/.eslintrc.js
  7. 16 0
      admin/.gitignore
  8. 5 0
      admin/.travis.yml
  9. 21 0
      admin/LICENSE
  10. 98 0
      admin/README-zh.md
  11. 24 0
      admin/README.md
  12. 54 0
      admin/app.js
  13. 6 0
      admin/babel.config.js
  14. 35 0
      admin/build/index.js
  15. 24 0
      admin/jest.config.js
  16. 9 0
      admin/jsconfig.json
  17. 67 0
      admin/mock/index.js
  18. 68 0
      admin/mock/mock-server.js
  19. 29 0
      admin/mock/table.js
  20. 84 0
      admin/mock/user.js
  21. 72 0
      admin/package.json
  22. 8 0
      admin/postcss.config.js
  23. BIN
      admin/public/favicon.ico
  24. 18 0
      admin/public/index.html
  25. 17 0
      admin/src/App.vue
  26. 26 0
      admin/src/api/agency.js
  27. 32 0
      admin/src/api/estate.js
  28. 66 0
      admin/src/api/house.js
  29. 66 0
      admin/src/api/index.js
  30. 37 0
      admin/src/api/qqmap.js
  31. 34 0
      admin/src/api/region.js
  32. 26 0
      admin/src/api/store.js
  33. 39 0
      admin/src/api/upload.js
  34. BIN
      admin/src/assets/avatar.png
  35. 46 0
      admin/src/assets/css/custom.less
  36. 335 0
      admin/src/assets/css/global.less
  37. BIN
      admin/src/assets/img/tags_close_hover.png
  38. BIN
      admin/src/assets/login/img_4dage.png
  39. BIN
      admin/src/assets/login/img_4dage@2x.png
  40. BIN
      admin/src/assets/login/img_kor.png
  41. BIN
      admin/src/assets/login/img_kor@2x.png
  42. BIN
      admin/src/assets/login/img_loginele_mod1.png
  43. BIN
      admin/src/assets/login/img_loginele_mod1@2x.png
  44. BIN
      admin/src/assets/login/img_loginele_mod2.png
  45. BIN
      admin/src/assets/login/img_loginele_mod2@2x.png
  46. BIN
      admin/src/assets/login/img_loginele_points.png
  47. BIN
      admin/src/assets/login/img_loginele_points@2x.png
  48. BIN
      admin/src/assets/logo/img_edit_logo.png
  49. BIN
      admin/src/assets/logo/img_edit_logo@2x.png
  50. 373 0
      admin/src/components/LoginForm.vue
  51. 60 0
      admin/src/components/Modal/index.vue
  52. 133 0
      admin/src/components/editPassword/index.vue
  53. 32 0
      admin/src/components/formCard/index.vue
  54. 12 0
      admin/src/components/index.js
  55. 187 0
      admin/src/components/map/index.vue
  56. 96 0
      admin/src/components/tables/handle-btns.vue
  57. 2 0
      admin/src/components/tables/index.js
  58. 264 0
      admin/src/components/tables/tables.vue
  59. 134 0
      admin/src/components/upload/index.vue
  60. 125 0
      admin/src/components/upload/video.vue
  61. 20 0
      admin/src/config/agency.js
  62. 0 0
      admin/src/config/apiCodeMsg.js
  63. 72 0
      admin/src/config/house.js
  64. 10 0
      admin/src/config/url.js
  65. 12 0
      admin/src/directive/index.js
  66. 178 0
      admin/src/layout/index/index.vue
  67. 3 0
      admin/src/layout/login/index.js
  68. 135 0
      admin/src/layout/login/login.vue
  69. 9 0
      admin/src/layout/makeLayout/index.js
  70. 95 0
      admin/src/layout/makeLayout/makeLayout.vue
  71. 71 0
      admin/src/libs/request.js
  72. 53 0
      admin/src/libs/token.js
  73. 160 0
      admin/src/libs/tools.js
  74. 35 0
      admin/src/locale/index.js
  75. 109 0
      admin/src/locale/zh-CN.js
  76. 35 0
      admin/src/main.js
  77. 34 0
      admin/src/plugin/index.js
  78. 25 0
      admin/src/register/index.js
  79. 73 0
      admin/src/router/index.js
  80. 88 0
      admin/src/router/routes.js
  81. 23 0
      admin/src/store/app.js
  82. 34 0
      admin/src/store/index.js
  83. 101 0
      admin/src/store/user.js
  84. 20 0
      admin/src/utils/date.js
  85. 34 0
      admin/src/utils/encode.js
  86. 0 0
      admin/src/views/enterprise/index.vue
  87. 21 0
      admin/src/views/system/address/components/create.vue
  88. 117 0
      admin/src/views/system/address/components/form.vue
  89. 122 0
      admin/src/views/system/address/list.vue
  90. 9 0
      admin/src/views/system/index.vue
  91. 31 0
      admin/src/views/system/store/components/create.vue
  92. 41 0
      admin/src/views/system/store/components/edit.vue
  93. 60 0
      admin/src/views/system/store/components/form.vue
  94. 129 0
      admin/src/views/system/store/list.vue
  95. 151 0
      admin/src/views/system/user/list.vue
  96. 5 0
      admin/tests/unit/.eslintrc.js
  97. 98 0
      admin/tests/unit/components/Breadcrumb.spec.js
  98. 18 0
      admin/tests/unit/components/Hamburger.spec.js
  99. 22 0
      admin/tests/unit/components/SvgIcon.spec.js
  100. 0 0
      admin/tests/unit/utils/formatTime.spec.js

+ 14 - 0
admin/.editorconfig

@@ -0,0 +1,14 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 15 - 0
admin/.env.development

@@ -0,0 +1,15 @@
+# just a flag
+ENV = 'development'
+
+# base api
+VUE_APP_BASE_API = '/dev-api'
+
+# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
+# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
+# It only does one thing by converting all import() to require().
+# This configuration can significantly increase the speed of hot updates,
+# when you have a large number of pages.
+# Detail:  https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
+
+VUE_CLI_BABEL_TRANSPILE_MODULES = true
+VUE_APP_4DKANKAN_URL = https://test.4dkankan.com

+ 6 - 0
admin/.env.production

@@ -0,0 +1,6 @@
+# just a flag
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = '/prod-api'
+VUE_APP_4DKANKAN_URL = https://www.4dkankan.com

+ 7 - 0
admin/.env.staging

@@ -0,0 +1,7 @@
+NODE_ENV = production
+
+# just a flag
+ENV = 'staging'
+
+VUE_APP_4DKANKAN_URL = https://test.4dkankan.com
+

+ 4 - 0
admin/.eslintignore

@@ -0,0 +1,4 @@
+build/*.js
+src/assets
+public
+dist

+ 1 - 0
admin/.eslintrc.js

@@ -0,0 +1 @@
+module.exports = {}

+ 16 - 0
admin/.gitignore

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

+ 5 - 0
admin/.travis.yml

@@ -0,0 +1,5 @@
+language: node_js
+node_js: 10
+script: npm run test
+notifications:
+  email: false

+ 21 - 0
admin/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-present PanJiaChen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

Datei-Diff unterdrückt, da er zu groß ist
+ 98 - 0
admin/README-zh.md


+ 24 - 0
admin/README.md

@@ -0,0 +1,24 @@
+# VR看房运营后台
+
+## 项目运行步骤
+
+
+```bash
+
+npm install
+
+# develop
+npm run dev
+```
+
+访问 http://localhost:9528
+
+## 构建
+
+```bash
+# 测试环境
+npm run build:stage
+
+# 生产环境
+npm run build:prod
+```

+ 54 - 0
admin/app.js

@@ -0,0 +1,54 @@
+let fs = require('fs')
+let path = require('path')
+
+
+// 获取命令行参数
+let parm = process.argv.splice(2)
+// 第一个参数是路径
+let rootPath = parm[0]
+// 后面的所有参数都是文件后缀
+let types = parm.splice(1)
+// 需要过滤的文件夹
+let filter = ['./node_modules']
+// 统计结果
+let num = 0
+
+
+// 获取行数
+async function line(path) {
+    let rep = await fs.readFileSync(path)
+    rep = rep.toString()
+    let lines = rep.split('\n')
+    console.log(path + ' ' + lines.length)
+    num += lines.length
+}
+
+
+// 递归所有文件夹统计
+async function start(pt) {
+    let files = fs.readdirSync(pt)
+    files
+        .map(file => {
+            return `${pt}/${file}`
+        })
+        .forEach(file => {
+            let stat = fs.statSync(file)
+            if (stat.isDirectory()) {
+                if (filter.indexOf(pt) != -1) {
+                    return
+                }
+                start(file)
+                return
+            }
+            let ext = path.extname(file)
+            if (types.indexOf(ext) != -1) {
+                line(file)
+            }
+        })
+}
+
+
+;(async () => {
+    await start(rootPath)
+    console.log(`总代码行数:${num}`)
+})()

+ 6 - 0
admin/babel.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  presets: [
+    '@vue/app'
+  ],
+  plugins: ['syntax-dynamic-import']
+}

+ 35 - 0
admin/build/index.js

@@ -0,0 +1,35 @@
+const { run } = require('runjs')
+const chalk = require('chalk')
+const config = require('../vue.config.js')
+const rawArgv = process.argv.slice(2)
+const args = rawArgv.join(' ')
+
+if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
+  const report = rawArgv.includes('--report')
+
+  run(`vue-cli-service build ${args}`)
+
+  const port = 9526
+  const publicPath = config.publicPath
+
+  var connect = require('connect')
+  var serveStatic = require('serve-static')
+  const app = connect()
+
+  app.use(
+    publicPath,
+    serveStatic('./dist', {
+      index: ['index.html', '/']
+    })
+  )
+
+  app.listen(port, function () {
+    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
+    if (report) {
+      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
+    }
+
+  })
+} else {
+  run(`vue-cli-service build ${args}`)
+}

+ 24 - 0
admin/jest.config.js

@@ -0,0 +1,24 @@
+module.exports = {
+  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
+  transform: {
+    '^.+\\.vue$': 'vue-jest',
+    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
+      'jest-transform-stub',
+    '^.+\\.jsx?$': 'babel-jest'
+  },
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1'
+  },
+  snapshotSerializers: ['jest-serializer-vue'],
+  testMatch: [
+    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
+  ],
+  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
+  coverageDirectory: '<rootDir>/tests/unit/coverage',
+  // 'collectCoverage': true,
+  'coverageReporters': [
+    'lcov',
+    'text-summary'
+  ],
+  testURL: 'http://localhost/'
+}

+ 9 - 0
admin/jsconfig.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+        "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

+ 67 - 0
admin/mock/index.js

@@ -0,0 +1,67 @@
+import Mock from 'mockjs'
+import { param2Obj } from '../src/utils'
+
+import user from './user'
+import table from './table'
+
+const mocks = [
+  ...user,
+  ...table
+]
+
+// for front mock
+// please use it cautiously, it will redefine XMLHttpRequest,
+// which will cause many of your third-party libraries to be invalidated(like progress event).
+export function mockXHR() {
+  // mock patch
+  // https://github.com/nuysoft/Mock/issues/300
+  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
+  Mock.XHR.prototype.send = function() {
+    if (this.custom.xhr) {
+      this.custom.xhr.withCredentials = this.withCredentials || false
+
+      if (this.responseType) {
+        this.custom.xhr.responseType = this.responseType
+      }
+    }
+    this.proxy_send(...arguments)
+  }
+
+  function XHR2ExpressReqWrap(respond) {
+    return function(options) {
+      let result = null
+      if (respond instanceof Function) {
+        const { body, type, url } = options
+        // https://expressjs.com/en/4x/api.html#req
+        result = respond({
+          method: type,
+          body: JSON.parse(body),
+          query: param2Obj(url)
+        })
+      } else {
+        result = respond
+      }
+      return Mock.mock(result)
+    }
+  }
+
+  for (const i of mocks) {
+    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
+  }
+}
+
+// for mock server
+const responseFake = (url, type, respond) => {
+  return {
+    url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
+    type: type || 'get',
+    response(req, res) {
+      console.log('request invoke:' + req.path)
+      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
+    }
+  }
+}
+
+export default mocks.map(route => {
+  return responseFake(route.url, route.type, route.response)
+})

+ 68 - 0
admin/mock/mock-server.js

@@ -0,0 +1,68 @@
+const chokidar = require('chokidar')
+const bodyParser = require('body-parser')
+const chalk = require('chalk')
+const path = require('path')
+
+const mockDir = path.join(process.cwd(), 'mock')
+
+function registerRoutes(app) {
+  let mockLastIndex
+  const { default: mocks } = require('./index.js')
+  for (const mock of mocks) {
+    app[mock.type](mock.url, mock.response)
+    mockLastIndex = app._router.stack.length
+  }
+  const mockRoutesLength = Object.keys(mocks).length
+  return {
+    mockRoutesLength: mockRoutesLength,
+    mockStartIndex: mockLastIndex - mockRoutesLength
+  }
+}
+
+function unregisterRoutes() {
+  Object.keys(require.cache).forEach(i => {
+    if (i.includes(mockDir)) {
+      delete require.cache[require.resolve(i)]
+    }
+  })
+}
+
+module.exports = app => {
+  // es6 polyfill
+  require('@babel/register')
+
+  // parse app.body
+  // https://expressjs.com/en/4x/api.html#req.body
+  app.use(bodyParser.json())
+  app.use(bodyParser.urlencoded({
+    extended: true
+  }))
+
+  const mockRoutes = registerRoutes(app)
+  var mockRoutesLength = mockRoutes.mockRoutesLength
+  var mockStartIndex = mockRoutes.mockStartIndex
+
+  // watch files, hot reload mock server
+  chokidar.watch(mockDir, {
+    ignored: /mock-server/,
+    ignoreInitial: true
+  }).on('all', (event, path) => {
+    if (event === 'change' || event === 'add') {
+      try {
+        // remove mock routes stack
+        app._router.stack.splice(mockStartIndex, mockRoutesLength)
+
+        // clear routes cache
+        unregisterRoutes()
+
+        const mockRoutes = registerRoutes(app)
+        mockRoutesLength = mockRoutes.mockRoutesLength
+        mockStartIndex = mockRoutes.mockStartIndex
+
+        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
+      } catch (error) {
+        console.log(chalk.redBright(error))
+      }
+    }
+  })
+}

+ 29 - 0
admin/mock/table.js

@@ -0,0 +1,29 @@
+import Mock from 'mockjs'
+
+const data = Mock.mock({
+  'items|30': [{
+    id: '@id',
+    title: '@sentence(10, 20)',
+    'status|1': ['published', 'draft', 'deleted'],
+    author: 'name',
+    display_time: '@datetime',
+    pageviews: '@integer(300, 5000)'
+  }]
+})
+
+export default [
+  {
+    url: '/vue-admin-template/table/list',
+    type: 'get',
+    response: config => {
+      const items = data.items
+      return {
+        code: 20000,
+        data: {
+          total: items.length,
+          items: items
+        }
+      }
+    }
+  }
+]

+ 84 - 0
admin/mock/user.js

@@ -0,0 +1,84 @@
+
+const tokens = {
+  admin: {
+    token: 'admin-token'
+  },
+  editor: {
+    token: 'editor-token'
+  }
+}
+
+const users = {
+  'admin-token': {
+    roles: ['admin'],
+    introduction: 'I am a super administrator',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  'editor-token': {
+    roles: ['editor'],
+    introduction: 'I am an editor',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  }
+}
+
+export default [
+  // user login
+  {
+    url: '/vue-admin-template/user/login',
+    type: 'post',
+    response: config => {
+      const { username } = config.body
+      const token = tokens[username]
+
+      // mock error
+      if (!token) {
+        return {
+          code: 60204,
+          message: 'Account and password are incorrect.'
+        }
+      }
+
+      return {
+        code: 20000,
+        data: token
+      }
+    }
+  },
+
+  // get user info
+  {
+    url: '/vue-admin-template/user/info\.*',
+    type: 'get',
+    response: config => {
+      const { token } = config.query
+      const info = users[token]
+
+      // mock error
+      if (!info) {
+        return {
+          code: 50008,
+          message: 'Login failed, unable to get user details.'
+        }
+      }
+
+      return {
+        code: 20000,
+        data: info
+      }
+    }
+  },
+
+  // user logout
+  {
+    url: '/vue-admin-template/user/logout',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 20000,
+        data: 'success'
+      }
+    }
+  }
+]

+ 72 - 0
admin/package.json

@@ -0,0 +1,72 @@
+{
+  "name": "vue-admin-template",
+  "version": "4.2.1",
+  "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
+  "author": "Pan <panfree23@gmail.com>",
+  "license": "MIT",
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build:prod": "vue-cli-service build",
+    "build:stage": "vue-cli-service build --mode staging",
+    "preview": "node build/index.js --preview",
+    "lint": "eslint --ext .js,.vue src",
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
+    "test:ci": "npm run lint && npm run test:unit",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
+  },
+  "dependencies": {
+    "axios": "^0.19.2",
+    "element-ui": "2.13.0",
+    "js-base64": "^2.5.2",
+    "js-cookie": "2.2.0",
+    "js-pinyin": "^0.1.9",
+    "jsonp": "^0.2.1",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "path-to-regexp": "2.4.0",
+    "view-design": "^4.2.0",
+    "vue": "2.6.10",
+    "vue-i18n": "^8.17.7",
+    "vue-router": "3.0.6",
+    "vuex": "3.1.0"
+  },
+  "devDependencies": {
+    "@babel/core": "7.0.0",
+    "@babel/register": "7.0.0",
+    "@vue/cli-plugin-babel": "3.6.0",
+    "@vue/cli-plugin-eslint": "^3.9.1",
+    "@vue/cli-plugin-unit-jest": "3.6.3",
+    "@vue/cli-service": "3.6.0",
+    "@vue/test-utils": "1.0.0-beta.29",
+    "autoprefixer": "^9.8.0",
+    "babel-core": "7.0.0-bridge.0",
+    "babel-eslint": "10.0.1",
+    "babel-jest": "23.6.0",
+    "babel-plugin-syntax-dynamic-import": "^6.18.0",
+    "chalk": "2.4.2",
+    "connect": "3.6.6",
+    "eslint": "5.15.3",
+    "eslint-plugin-vue": "5.2.2",
+    "html-webpack-plugin": "3.2.0",
+    "less": "^3.11.1",
+    "less-loader": "^5.0.0",
+    "mockjs": "1.0.1-beta3",
+    "node-sass": "^4.14.1",
+    "runjs": "^4.3.2",
+    "sass-loader": "^7.1.0",
+    "script-ext-html-webpack-plugin": "2.1.3",
+    "script-loader": "0.7.2",
+    "serve-static": "^1.13.2",
+    "svg-sprite-loader": "4.1.3",
+    "svgo": "1.2.2",
+    "vue-template-compiler": "2.6.10"
+  },
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ]
+}

+ 8 - 0
admin/postcss.config.js

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

BIN
admin/public/favicon.ico


+ 18 - 0
admin/public/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= webpackConfig.name %></title>
+    <script src="https://map.qq.com/api/gljs?v=1.exp&key=P47BZ-KX7W6-3ULSW-EJYUH-MC2GF-AOFC3"></script>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 17 - 0
admin/src/App.vue

@@ -0,0 +1,17 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App'
+}
+</script>
+
+<style lang="less">
+@import "~assets/css/global.less";
+@import "~assets/css/custom.less";
+@import "//at.alicdn.com/t/font_1997910_7jquslacvck.css";
+</style>

+ 26 - 0
admin/src/api/agency.js

@@ -0,0 +1,26 @@
+import request from 'libs/request'
+
+/*
+** @decs 获取小区列表
+*/
+export function fetchAgencyList (data) {
+  data.store = data.tools_0 || ''
+  data.keyword = data.searchKey
+  return request.get('agency/queryList', {params: data})
+}
+
+export function createAgency (data) {
+  return request.post('agency/add', data)
+}
+
+export function fetchAgencyDetail (agency_id) {
+  return request.get('agency/detail', { params: { agency_user_id: agency_id } })
+}
+
+export function updateAgency (data) {
+  return request.post('agency/update', data)
+}
+
+export function deleteAgency (strIds) {
+  return request.post('agency/delete', { strIds })
+}

+ 32 - 0
admin/src/api/estate.js

@@ -0,0 +1,32 @@
+import request from 'libs/request'
+
+/*
+** @decs 获取小区列表
+*/
+export function fetchEstateList (data={}) {
+  const defaultParams = {
+    page_size: 10,
+    page_num: 1,
+    province: '',
+    city: '',
+    district: '',
+    query_name: data.searchKey || ''
+  }
+  return request.get('estate/queryAll', {params: Object.assign(defaultParams, data)})
+}
+
+export function createEstate (data) {
+  return request.post('estate/add', data)
+}
+
+export function fetchEstateDetail (estate_id) {
+  return request.get('estate/detail', { params: {estate_id} })
+}
+
+export function updateEstate (data) {
+  return request.post('estate/update', data)
+}
+
+export function deleteEstate (strIds) {
+  return request.post('estate/delete', { strIds })
+}

+ 66 - 0
admin/src/api/house.js

@@ -0,0 +1,66 @@
+import request from 'libs/request'
+import { getToken } from 'libs/token'
+/*
+** @decs 获取房源列表
+*/
+export function fetchHouseList (data) {
+  data.estate_name = data.tools_0 || ''
+  data.sale_type = data.tools_1 || ''
+  data.query_name = data.searchKey || ''
+  return request.get('house/queryAll', {params: data})
+}
+
+export function createHouse (data) {
+  return request.post('house/add', data)
+}
+
+export function fetchHouseDetail (house_id) {
+  return request.get('house/house', { params: {house_id} })
+}
+
+export function updateHouse (data) {
+  return request.post('house/update', data)
+}
+
+export function fetchUnAgentHouse (params) {
+  const defaultParams = {
+    sale_type: 1,
+    page_size: 10,
+    page_num: 1,
+    query_name: ''
+  }
+  params = Object.assign(defaultParams, params)
+  return request.get('house/queryHouse', { params })
+}
+
+/*
+** @desc 增加关联经纪人
+** @params agency_user_id 经纪人id
+** @params house_ids Array 房源id集合
+*/
+export function attachAgency (data) {
+  return request.post('house/attachAgency', data)
+}
+
+/*
+** @desc 修改关联经纪人
+** @params agencyUserIdNew 准绑定经纪人id
+** @params agencyUserIdOld 老绑定经纪人id
+** @params house_id String 房源id
+*/
+export function changeAgency (data) {
+  return request.post('house/changeAgency', data)
+}
+export function deleteHouse (strIds) {
+  return request.post('house/delete', { strIds })
+}
+
+export function fetchAllScene (data) {
+  const defaultParams = {
+    page_num: 1,
+    page_size: 10,
+    searchKey: '',
+    token: getToken()
+  }
+  return request.post('house/getAllScene', Object.assign(defaultParams, data))
+}

+ 66 - 0
admin/src/api/index.js

@@ -0,0 +1,66 @@
+import request from 'libs/request'
+
+/*
+ * @Desc: 管理
+ */
+
+// 登录接口
+export function login(data) {
+  console.log(data, 'data')
+  return request({
+    url: 'login',
+    method: 'post',
+    data
+  })
+}
+
+// 注册接口
+export function register(data) {
+  return request({
+    url: 'register',
+    method: 'post',
+    data
+  })
+}
+
+// 获取验证码接口
+export function getCode(data) {
+  data.area_num = '86'
+  return request({
+    url: `getMsgAuthCode`,
+    method: 'get',
+    params: data
+  })
+}
+
+// 修改密码
+export function changePassword (data) {
+  return request({
+    url: `changePassword`,
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取管理员列表
+export function fetchUserList (params= {}) {
+  const defaultParams = {
+    query_name: params.searchKey,
+    page_size: 10,
+    page_num: 1
+  }
+  return request.get('listAdmin', {params: Object.assign(defaultParams, params)})
+}
+
+export function fetchRoleList () {
+  return request.get('sysRole/list')
+}
+// 获取首页统计信息
+export function getHomeData () {
+  return request.get('home')
+}
+
+export function updateUser (data) {
+  return request.post('changeInfo', data)
+}
+

+ 37 - 0
admin/src/api/qqmap.js

@@ -0,0 +1,37 @@
+
+import jsonp from 'jsonp'
+const BASEURL = 'https://apis.map.qq.com/ws/'
+
+function request (url, data={}) {
+  data.key = 'P47BZ-KX7W6-3ULSW-EJYUH-MC2GF-AOFC3&output=jsonp'
+  url = BASEURL + url + '?'
+  Object.keys(data).forEach(item => url += `${item}=${data[item]}&`)
+  return new Promise((resolve, reject) => {
+    jsonp(url, null, (err, data) => {
+      if (err) {
+        reject(err)
+        return
+      }
+      resolve(data)
+    })
+  })
+}
+export function getDistrictList () {
+  return request(`district/v1/list`, {})
+}
+
+export function searchPlaceByKeyword ({keyword, boundary}) {
+  return request('place/v1/search', {keyword, boundary})
+}
+
+export function getLocationByIp () {
+  return request('location/v1/ip')
+}
+
+export function getLocationByGeocoder (location) {
+  return request('geocoder/v1', { location })
+}
+
+export function getDistrict (keyword) {
+  return request('district/v1/search', { keyword })
+}

+ 34 - 0
admin/src/api/region.js

@@ -0,0 +1,34 @@
+import request from 'libs/request'
+
+export function fetchCityList (params) {
+  const defalutParams = {
+    type: '',
+    query_name: params.searchKey || '',
+  }
+  return request.get('region/list', {params: Object.assign(defalutParams, params)})
+}
+
+export function createCity (data) {
+  return request.post('region/save', data)
+}
+
+export function fetchChildCity (parent_id) {
+  return request.get('region/child/list', {params: {parent_id}})
+}
+
+export function delRegion (ids) {
+  return request.post('region/delete', { ids })
+}
+
+export function fetchRegionDetail (region_id) {
+  return request.get(`region/info/${region_id}`)
+}
+
+export function updateRegion (data) {
+  return request.post('region/update', data)
+}
+
+export function deleteRegion (strIds) {
+  return request.post('region/delete', { strIds })
+}
+

+ 26 - 0
admin/src/api/store.js

@@ -0,0 +1,26 @@
+import request from 'libs/request'
+
+export function fetchStoreList (params) {
+  const defaultParams = {
+    keyword: params.searchKey || '',
+    page_size: 10,
+    page_num: 1
+  }
+  return request.get('store/queryList', {params: Object.assign(defaultParams, params)})
+}
+
+export function createStore (data) {
+  return request.post('store/add', data)
+}
+
+export function updateStore (data) {
+  return request.post('store/update', data)
+}
+
+export function fetchAllStore (params) {
+  return request.get('store/all', {params})
+}
+
+export function deleteStore (intIds) {
+  return request.post('store/delete', { intIds })
+}

+ 39 - 0
admin/src/api/upload.js

@@ -0,0 +1,39 @@
+import request from 'libs/request'
+// const UploadUrl = '//127.0.0.1:1935'
+export function uploadFile (file, opts) {
+  if (!file) return new Promise(resolve => resolve())
+  if (file.type === 'image/png' || file.type === 'image/jpeg') {
+    return uploadPic(file, opts)
+  }
+  return uploadVideo(file)
+}
+
+export function uploadPic (file, opts) {
+  let formData = new FormData()
+  formData.append('file', file)
+  if (opts) {
+    Object.keys(opts).forEach(item => {
+      formData.append(item, opts[item])
+    })
+  }
+  return request.post(`../node-upload/uploadfile`, formData)
+}
+
+// export function uploadPic (file) {
+//   let formData = new FormData()
+//   formData.append('file', file)
+//   formData.append('quality', 90)
+//   return request.post(`../node-upload/localfile`, formData)
+// }
+
+
+export function uploadVideo (file) {
+  let formData = new FormData()
+
+  formData.append('file', file)
+  return request.post(`house/upLoadVideo`, formData)
+}
+
+export function getVideoFirstImage (object_name) {
+  return request.get('house/getFirstImage', { params: { object_name }})
+}

BIN
admin/src/assets/avatar.png


+ 46 - 0
admin/src/assets/css/custom.less

@@ -0,0 +1,46 @@
+/*
+ * @Author: Zp 定制主题样式
+ * @Date: 2020-02-25 14:07:09 
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-03-23 15:48:05
+ */
+
+@import "~view-design/src/styles/index.less";
+
+// Base Color
+@primary-color: #1FE4DC;
+@error-color: #f56c6c;
+@text-color: rgba(255, 255, 255, 0.88);
+@border-color-base: #555; // outside
+@border-color-split: #555; // inside
+@tooltip-color: #000;
+
+// Input
+// @input-height-base: 32px;
+// Button
+@btn-height-base: 32px;
+@padding-md: 21px; // small containers and buttons
+
+@input-bg: #161a1a;
+@input-hover-border-color: #000;
+@input-focus-border-color: #000;
+
+// Table
+@table-thead-bg: #252828;
+@table-td-stripe-bg: #252828;
+@table-td-hover-bg: #161a1a;
+@table-td-highlight-bg: #161a1a;
+
+// 菜单dark主题标题背景颜色
+@menu-dark-title: #252828;
+@layout-header-background: #252828;
+
+// 头部banner高度
+@layout-header-height: 50px;
+@layout-header-padding: 0 26px;
+
+// 边侧menu菜单定制颜色
+@menu-dark-title: #252828;
+@menu-dark-active-bg: #161a1a;
+@menu-dark-subsidiary-color: rgba(255, 255, 255, 0.7);
+@menu-dark-group-title-color: rgba(255, 255, 255, 0.38);

+ 335 - 0
admin/src/assets/css/global.less

@@ -0,0 +1,335 @@
+/*
+ * @Author: Zp 全局样式
+ * @Date: 2020-02-25 14:31:06 
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-04-27 13:30:27
+ */
+
+html,
+body,
+#app {
+  height: 100%;
+  font-family: Microsoft YaHei;
+  .ivu-input-large,.ivu-btn-large {
+    font-size: 14px;
+  }
+  .ivu-select-large.ivu-select-single .ivu-select-selection .ivu-select-placeholder, .ivu-select-large.ivu-select-single .ivu-select-selection .ivu-select-selected-value {
+    font-size: 14px;
+  }
+  .ivu-btn {
+    // border-color: #1FE4DC;
+    // background-color: transparent;
+    // color: #1FE4DC;
+  }
+  .ivu-btn-primary {
+    background-color: #1FE4DC;
+    color: #fff;
+  }
+  .ivu-btn-ghost.ivu-btn-primary {
+    color: #1FE4DC;
+    background: transparent;
+    border-color: #1FE4DC;
+  }
+  .ivu-input[disabled] {
+    background: #161a1a;
+    color: #fff;
+  }
+  th .ivu-table-cell {
+    color:rgba(255,255,255,0.38);
+    font-weight: normal;
+  }
+  .ivu-input-word-count,.ivu-input-group-append {
+    background: #161a1a;
+    
+  }
+  .ivu-input-group-append {
+    border-left: none;
+    position: relative;
+    font-size: 14px;
+    &::before {
+      content: '';
+      width: 1px;
+      height: 100%;
+      display: block;
+      background: #161a1a;
+      position: absolute;
+      left: -1px;
+      top: 0;
+      z-index: 111;
+    }
+  }
+  .ivu-date-picker .ivu-select-dropdown {
+    background: #161a1a;
+  }
+  .ivu-modal-header {
+    padding: 0;
+  }
+  .cancle-btn {
+    border: 1px solid #555555;
+    background: transparent;
+    color: #fff;
+    margin-right: 14px;
+  }
+  .ivu-select-large .ivu-select-input {
+    font-size: 14px;
+  }
+  .upload-handle, .file-item {
+    width: 100px;
+    height: 100px;
+    display: inline-block;
+    position: relative;
+    margin-left: 10px;
+    img {
+      width: 100px;
+      height: 100px;
+    }
+    .close-btn {
+      color: #fff;
+      font-size: 12px !important;
+      position: absolute;
+      right: 0;
+      top: 0;
+      line-height: 1;
+      .close {
+        font-size: 12px;
+      }
+    }
+  }
+  .cancle-btn {
+    width: 70px;
+    background-color: transparent;
+    &:hover {
+      background-color: transparent;
+    }
+  }
+  .ivu-select-multiple .ivu-select-item-focus, .ivu-select-multiple .ivu-select-item-selected:hover, .ivu-select-multiple .ivu-select-item-selected {
+    background-color: transparent;
+  }
+  .ivu-select-large.ivu-select-multiple .ivu-tag {
+    background-color: transparent;
+  }
+}
+
+// 覆盖 view-design 默认样式 start
+
+// view Message 背景颜色
+.ivu-message-notice .ivu-message-notice-content {
+  background-color: #000;
+}
+
+// view Icon 手指
+.ivu-icon,
+.iconfont {
+  font-size: 20px;
+  cursor: pointer;
+}
+
+.ivu-icon:not(:last-child) {
+  margin-right: 5px;
+}
+
+// view Poptip 提示气泡全局样式
+.ivu-poptip-content {
+  .ivu-poptip-inner {
+    background-color: #161a1a;
+  }
+}
+
+.ivu-poptip-popper[x-placement^="top"]
+  .ivu-poptip-content
+  .ivu-poptip-arrow:after {
+  border-top-color: #161a1a;
+}
+
+// view 对话框默认样式
+.ivu-modal {
+  .ivu-modal-content {
+    background-color: rgb(40, 39, 42);
+    .ivu-modal-confirm-head-title {
+      color: #fff;
+    }
+  }
+  .ivu-modal-header,
+  .ivu-modal-footer {
+    border: none;
+    p {
+      color: #fff;
+    }
+  }
+}
+
+// view 表格默认样式
+.ivu-table-wrapper {
+  .ivu-table,
+  .ivu-table td {
+    background-color: transparent;
+    .ivu-table-row td {
+      border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+      background-color: #252828;
+    }
+  }
+}
+
+// view 下拉框默认样式
+.ivu-select-dropdown.ivu-select-dropdown-transfer {
+  background-color: #161a1a;
+  .ivu-select-item:hover, .ivu-select-item-focus {
+    background: #161a1a;
+  }
+}
+
+.ivu-select {
+  .ivu-select-selection {
+    border: 1px solid #555a5a;
+    background-color: #161a1a;
+  }
+  .ivu-select-dropdown {
+    max-height: 300px;
+    background-color: #161a1a;
+  }
+  .ivu-select-item-selected,
+  .ivu-select-item-selected:hover {
+    color: #1FE4DC;
+  }
+  .ivu-select-item-focus,
+  .ivu-select-item:hover {
+    background-color: #252828;
+  }
+}
+// view 输入框、下拉框,伪类默认样式
+// .ivu-input-wrapper .ivu-input:hover,
+// .ivu-input-wrapper .ivu-input:focus,
+// .ivu-select .ivu-select-selection:hover,
+// .ivu-select .ivu-select-selection-focused {
+//   border: 1px solid #000;
+//   box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.2);
+// }
+
+// view 上传
+.ivu-upload .ivu-upload-drag {
+  width: 100%;
+  height: 100%;
+  background-color: transparent;
+  .upload {
+    padding: 15px 0;
+  }
+}
+
+// view 分页默认样式
+.ivu-page {
+  li {
+    background-color: transparent;
+  }
+}
+
+
+
+// 覆盖 view-design 默认样式 end
+
+// 自定义 class 全局样式 start
+
+// 全局搜索框样式
+.search-box {
+  width: 240px;
+  height: 40px;
+  margin-bottom: 20px;
+  .ivu-input {
+    padding: 11px 15px;
+  }
+}
+
+.button-group {
+  button:not(:last-child) {
+    margin-right: 15px;
+  }
+}
+
+// 布局主体容器
+.layout-main {
+  overflow: auto;
+  padding: 24px;
+  border-left: 1px solid #555a5a;
+  background-color: #161a1a;
+}
+
+// 主体容器滚动条
+.layout-main::-webkit-scrollbar {
+  width: 4px;
+}
+.layout-main::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.38);
+}
+
+// 自定义上传信息框滚动条样式
+*::-webkit-scrollbar {
+  width: 4px;
+}
+*::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.38);
+}
+
+// 自定义记住密码表单框的样式
+input:-webkit-autofill,
+textarea:-webkit-autofill,
+select:-webkit-autofill {
+  -webkit-text-fill-color: #ededed !important;
+  -webkit-box-shadow: 0 0 0px 1000px transparent inset !important;
+  background-color: transparent;
+  background-image: none;
+  transition: background-color 50000s ease-in-out 0s; //背景色透明  生效时长  过渡效果  启用时延迟的时间
+}
+
+// 布局容器
+.layout-container {
+  padding: 20px;
+  background-color: #252828;
+}
+
+// 自定义 class 全局样式 end
+.clearfix {
+  clear: both;
+  &:before,&:after {
+    content:"";
+    display:table;
+ }
+ &:after { clear:both; }
+}
+.fl {
+  float: left;
+}
+.fr {
+  float: right;
+}
+.app-container {
+  background-color: #252828;
+}
+
+.form-content {
+  padding: 20px;
+  background:rgba(255,255,255,0.05);
+}
+
+.m-r-12 {
+  margin-right: 12px;
+}
+.upload-tip {
+  color:rgba(255,255,255,0.38);
+  font-size: 12px;
+  line-height: 1;
+}
+
+.upload-box {
+  width: 500px;
+}
+.house-upload {
+  width: 100px;
+  height: 100px;
+  display: inline-block;
+}
+.hidden-input {
+  position: fixed;
+  left: 99999px;
+  top: 99999px;
+}

BIN
admin/src/assets/img/tags_close_hover.png


BIN
admin/src/assets/login/img_4dage.png


BIN
admin/src/assets/login/img_4dage@2x.png


BIN
admin/src/assets/login/img_kor.png


BIN
admin/src/assets/login/img_kor@2x.png


BIN
admin/src/assets/login/img_loginele_mod1.png


BIN
admin/src/assets/login/img_loginele_mod1@2x.png


BIN
admin/src/assets/login/img_loginele_mod2.png


BIN
admin/src/assets/login/img_loginele_mod2@2x.png


BIN
admin/src/assets/login/img_loginele_points.png


BIN
admin/src/assets/login/img_loginele_points@2x.png


BIN
admin/src/assets/logo/img_edit_logo.png


BIN
admin/src/assets/logo/img_edit_logo@2x.png


+ 373 - 0
admin/src/components/LoginForm.vue

@@ -0,0 +1,373 @@
+<template>
+  <div class="login-form">
+    <div class="login-form-title">{{ activeModel.title }}</div>
+    <div class="login-form-account">
+      <input class="hidden-input" >
+        
+      <input class="hidden-input" type="password" name="password1"  />
+      <div v-for="item in activeModel.item" :key="item.name" class="form-item">
+        {{ item.name }}
+        
+        <input
+          v-model="loginForm[item.model]"
+          :type="item.type"
+          @keyup.enter="handleLogin"
+          autocomplete="off"
+        >
+        <span class="sendCode-btn" v-if="item.code" @click="handleGetCode">{{sendCodeSecend ? `${sendCodeSecend}s后重试` : '获取验证码'}}</span>
+      </div>
+      <div v-if="activeModel.type === 'login'" class="form-subjoin login-sub">
+        <a class="forget" target="_blank" href="https://4dkankan.com">忘记密码</a>
+        <a class="register" target="_blank" href="https://4dkankan.com">账号注册</a>
+      </div>
+      <div v-else-if="activeModel.type === 'register'" class="form-subjoin">
+        已有账号?
+         <router-link to="login" class="login-up">登录</router-link>
+      </div>
+      <div v-else class="form-subjoin">
+        想起来了,
+         <router-link to="login" class="login-up">登录</router-link>
+      </div>
+      <div class="form-button">
+        <Button type="primary" long @click="handles(activeModel.handle)">{{ activeModel.buttonText }}</Button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Message } from 'view-design'
+import { mapActions } from 'vuex'
+import { register, getCode, changePassword } from 'api'
+import encode from '@/utils/encode'
+  
+export default {
+  name: 'LoginForm',
+  components: {},
+  data() {
+    return {
+      sendCodeSecend: 0,
+      loginForm: {
+        nickName: '',
+        password: '',
+        confirmPwd: '',
+        phoneNum: '',
+        msgAuthCode: '',
+        country: '中国',
+        randomcode: '',
+        rememberMe: true
+      },
+      loginModel: {
+        type: 'login',
+        handle: 'handleLogin',
+        title: '后台登录',
+        buttonText: '登录',
+        item: [{
+          name: '账号',
+          type: 'text',
+          model: 'phoneNum'
+        }, {
+          name: '密码',
+          type: 'password',
+          model: 'password'
+        }]
+      },
+      registerModel: {
+        type: 'register',
+        handle: 'handleRegister',
+        title: '账号注册',
+        buttonText: '注册',
+        item: [{
+          name: '手机号',
+          type: 'text',
+          model: 'phoneNum',
+          errorMsg: '请填写手机号'
+        }, {
+          name: '验证码',
+          type: 'text',
+          code: true,
+          model: 'msgAuthCode',
+          errorMsg: '请填写验证码'
+        }, {
+          name: '输入密码',
+          type: 'password',
+          model: 'password',
+          errorMsg: '请填写密码'
+        }, {
+          name: '确认密码',
+          type: 'password',
+          model: 'confirmPwd',
+          errorMsg: '请填写确认密码'
+        }]
+      },
+      forgetModel: {
+        type: 'forget',
+        handle: 'handleForget',
+        title: '密码重置',
+        buttonText: '重置',
+        item: [{
+          name: '手机号',
+          type: 'text',
+          model: 'phoneNum'
+        }, {
+          name: '验证码',
+          type: 'text',
+          code: true,
+          model: 'msgAuthCode'
+        }, {
+          name: '新密码',
+          type: 'password',
+          model: 'password'
+        }, {
+          name: '确认密码',
+          type: 'password',
+          model: 'confirmPwd'
+        }]
+      }
+    }
+  },
+  computed: {
+    activeModel() {
+      const type = this.$route.query.type ? this.$route.query.type : 'login'
+      return this[type + 'Model']
+    }
+  },
+  watch: {
+    activeModel (val) {
+      let phoneNum = this.loginForm.phoneNum
+      this.resetForm()
+      this.loginForm.phoneNum = phoneNum
+    }
+  },
+  mounted () {
+    this.$nextTick(() => {
+      this.resetForm()
+    })
+  },
+  methods: {
+    ...mapActions([
+      'login'
+    ]),
+    resetForm() {
+      this.loginForm = {
+        nickName: '',
+        password: '',
+        confirmPwd: '',
+        phoneNum: '',
+        msgAuthCode: '',
+        country: '中国',
+        randomcode: '',
+        rememberMe: true
+      }
+    },
+    handles(handle) {
+      this[handle]()
+    },
+    handleGetCode() {
+      this.$bebounce(() => {
+        if (this.sendCodeSecend) return
+        console.log(this.activeModel.type)
+        let typeMap = {
+          register: 1,
+          forget: 2
+        }
+        const type = typeMap[this.activeModel.type] || ''
+        getCode({ phone_num: this.loginForm.phoneNum, type }).then(
+          res => {
+            Message.info({
+              content: '验证码发送成功',
+              background: true
+            })
+            this.sendCodeSecend = 60
+            let timer
+            timer = setInterval(() => {
+              if (this.sendCodeSecend <= 1) {
+                clearInterval(timer)
+              }
+              this.sendCodeSecend = this.sendCodeSecend - 1
+            }, 1000)
+            // this.loginForm.code = res.data
+          }
+        ).catch (err => {
+          this.sendCodeSecend = 0
+        })
+      })
+      
+    },
+    handleLogin() {
+      if (!this.loginForm.phoneNum.trim()) {
+        Message.error({
+          content: '请输入账号',
+          background: true
+        })
+        return false
+      }
+      if (!this.loginForm.password.trim()) {
+        Message.error({
+          content: '请输入密码',
+          background: true
+        })
+        return false
+      }
+      let form = Object.assign({}, this.loginForm)
+      form.password = encode(this.loginForm.password)
+      this.login(form).then(
+        res => {
+          this.$router.push({ path: '/' })
+        }
+      )
+    },
+    handleRegister() {
+      if (!this.loginForm.phoneNum || !(/^1[3456789]\d{9}$/.test(this.loginForm.phoneNum))) {
+        Message.error({
+          content: '请输入正确的手机号',
+          background: true
+        })
+        return false
+      }
+      if (!this.loginForm.msgAuthCode || !(/\d{6}/.test(this.loginForm.msgAuthCode))) {
+        Message.error({
+          content: '请输入正确的验证码',
+          background: true
+        })
+        return false
+      }
+      if (!this.loginForm.password) {
+        Message.error({
+          content: '请输入密码',
+          background: true
+        })
+        return false
+      }
+      if (this.loginForm.password !== this.loginForm.confirmPwd) {
+        Message.error({
+          content: '两次输入的密码不一致'
+        })
+        return
+      }
+
+      this.loginForm.password = encode(this.loginForm.password)
+      this.loginForm.confirmPwd = encode(this.loginForm.confirmPwd)
+      register(this.loginForm).then(
+        () => {
+          Message.info({
+            content: '注册成功',
+            background: true
+          })
+          this.$router.push({ path: '/' })
+        }
+      ).catch((err) => {
+        console.log(err, 'err')
+        this.loginForm.password = ''
+        this.loginForm.confirmPwd = ''
+      })
+    },
+    handleForget() {
+      if (!this.loginForm.phoneNum || !(/^1[3456789]\d{9}$/.test(this.loginForm.phoneNum))) {
+        Message.error({
+          content: '请输入正确的手机号',
+          background: true
+        })
+        return false
+      }
+      if (!this.loginForm.msgAuthCode) {
+        Message.error({
+          content: '请输入验证码',
+          background: true
+        })
+        return false
+      }
+      if (!this.loginForm.password) {
+        Message.error({
+          content: '请输入密码',
+          background: true
+        })
+        return false
+      }
+      if (this.loginForm.password !== this.loginForm.confirmPwd) {
+        Message.error({
+          content: '两次输入的密码不一致'
+        })
+        return
+      }
+      this.loginForm.password = encode(this.loginForm.password)
+      this.loginForm.confirmPwd = this.loginForm.password
+      changePassword(Object.assign(this.loginForm)).then(
+        () => {
+          Message.info({
+            content: '重置成功',
+            background: true
+          })
+          this.resetForm()
+          this.$router.push({ path: 'login' })
+        }
+      ).catch((err) => {
+        console.log(err, 'err')
+        this.loginForm.password = ''
+        this.loginForm.confirmPwd = ''
+      })
+    }
+  },
+}
+</script>
+
+<style scoped lang="less">
+.login-form {
+  background-color: #252828;
+  border-radius: 2px;
+  &-title {
+    font-size: 16px;
+    height: 48px;
+    line-height: 48px;
+    text-align: center;
+    color: rgba(255, 255, 255, 0.88);
+    border-bottom: 1px solid #1FE4DC;
+  }
+  &-account {
+    position: relative;
+    padding: 30px;
+    .form-item {
+      width: 280px;
+      height: 48px;
+      padding: 10px;
+      border: 1px solid rgba(255, 255, 255, 0.1);
+      border-radius: 2px;
+      margin-bottom: 20px;
+      font-size: 14px;
+      color: #fff;
+      input {
+        width: 135px;
+        color: #fff;
+        padding: 4px;
+        background: none;
+        border: none;
+        outline: none;
+      }
+    }
+    .form-subjoin {
+      text-align: right;
+      margin-bottom: 20px;
+    }
+    .login-sub {
+      a {
+        color: rgba(255, 255, 255, 0.38);
+      }
+      a:nth-child(2):before {
+        content: "|";
+        margin: 0 8px;
+      }
+    }
+    .form-button {
+      button {
+        border-radius: 2px;
+        height: 48px;
+        background-color: #1FE4DC;
+      }
+    }
+  }
+}
+.sendCode-btn {
+  cursor: pointer;
+}
+
+</style>

+ 60 - 0
admin/src/components/Modal/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <Modal :width="width" v-model="show" footer-hide :mask-closable="false">
+    <div slot="header" class="modal-header">
+      {{ title }}
+    </div>
+    <div class="close" slot="close">
+      <Icon custom="iconfont iconform_close" @click="close" />
+    </div>
+    <div class="content">
+      <slot />
+      <div  class="btn-w">
+        <Button class="cancle-btn" @click="close" size="large">取消</Button>
+        <Button class="submit-btn" type="primary" size="large" @click="submit">{{submitText}}</Button>
+      </div>
+    </div>
+    
+  </Modal>
+</template>
+
+<script>
+export default {
+  props: {
+    title: String,
+    width: Number,
+    show: Boolean,
+    submitText: {
+      type: String,
+      default: '保存'
+    }
+  },
+  methods: {
+    close () {
+      this.$emit('close')
+    },
+    submit () {
+      this.$emit('submit')
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+.modal-header {
+  line-height: 50px;
+  padding-left: 20px;
+  font-size: 16px;
+}
+.btn-w {
+  text-align: right;
+  // padding-right: 10px;
+}
+.cancle-btn {
+  border: 1px solid #555555;
+  background: transparent;
+  color: #fff;
+  margin-right: 14px;
+}
+.content {
+  padding: 4px;
+}
+</style>

+ 133 - 0
admin/src/components/editPassword/index.vue

@@ -0,0 +1,133 @@
+<template>
+  
+  <div class="modal-conent clearfix">
+      
+      <Form :form="form" :label-width="75">
+        <FormItem label="手机号码">
+          <Input class="form-input" v-model="form.phoneNum" size="large" disabled />
+        </FormItem>
+        <FormItem label="验证码">
+          <template>
+            <Input class="form-input form-input-small" v-model="form.msgAuthCode" size="large" />
+            <Button size="large" type="primary" class="form-btn" ghost @click="sendCode">{{ sendCodeSecend ? `${sendCodeSecend}s后重试` : '获取'}}</Button>
+          </template>
+        </FormItem>
+        <FormItem label="新密码">
+          <input class="hidden-input" > 
+          <input class="hidden-input" type="password" name="password1"  />
+          <Input type="password"  v-model="form.password"  class="form-input" size="large" />
+        </FormItem>
+        <FormItem label="确认密码">
+          <Input type="password" v-model="form.confirmPwd"  class="form-input" size="large" />
+        </FormItem>
+      </Form>
+      <div class="btn-w fr">
+        <Button ghost size="large" @click="changeShow(false)">取消</Button>
+        <Button type="primary" size="large" class="submit-btn" @click="submit">提交</Button>
+      </div>
+    </div>
+</template>
+
+<script>
+import { changePassword, getCode } from 'api'
+import encode from '@/utils/encode'
+import { getAdmin } from 'libs/token'
+export default {
+  props: {
+    show: Boolean
+  },
+  data () {
+    return {
+      sendCodeSecend: 0,
+      form: {
+        confirmPwd: '',
+        phoneNum: getAdmin().phone,
+        msgAuthCode: '',
+        password: '',
+        country: '中国',
+        randomcode: '',
+        rememberMe: true,
+        nickname: ''
+      }
+    }
+  },
+  methods: {
+    sendCode () {
+      if (this.sendCodeSecend) return
+      getCode({ phone_num: this.form.phoneNum }).then(
+        res => {
+          this.$Message.info({
+            content: '验证码发送成功',
+            background: true
+          })
+          this.sendCodeSecend = 60
+          let timer
+          timer = setInterval(() => {
+            if (this.sendCodeSecend <= 1) {
+              clearInterval(timer)
+            }
+            this.sendCodeSecend = this.sendCodeSecend - 1
+          }, 1000)
+          // this.loginForm.code = res.data
+        }
+      )
+    },
+    submit () {
+      let form = Object.assign({}, this.form)
+      if (form.password !== form.confirmPwd) {
+        this.$Message.error({
+          content: '两次输入的密码不一致'
+        })
+        return
+      }
+      form.password = encode(form.password)
+      form.confirmPwd = form.password
+      changePassword(form).then(res => {
+        if (res.code == 0) {
+          this.$Message.success({
+            content: '修改密码成功',
+            onClose: () => {
+              this.$router.push('/login')
+            }
+          })
+        }
+      }).catch(err => {
+        console.log(err)
+        this.$Message.error({
+          content: '修改失败'
+        })
+      })
+    },
+    changeShow (show) {
+      this.$emit('change', show)
+    }
+  }
+}
+</script>
+
+<style lang="less">
+.edit-modal {
+  .modal-header {
+    font-size: 16px;
+    line-height: 55px;
+    padding-left: 20px;
+  }
+  .form-input {
+    width: 300px;
+  }
+  .form-input-small {
+    width: 187px;
+    margin-right: 15px;
+  }
+  .form-btn {
+    width: 98px;
+    padding: 0;
+    text-align: center;
+  }
+  .submit-btn {
+    margin: 0 15px;
+    width: 98px;
+  }
+}
+
+</style>

+ 32 - 0
admin/src/components/formCard/index.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="form-item-w">
+    <h4 class="form-title">{{ title }}</h4>
+    <div class="form-content">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    title: String
+  },
+  data () {
+    return {
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.form-title {
+  line-height: 55px;
+  color:rgba(255,255,255,0.38);
+  font-weight: normal;
+}
+.form-content {
+  padding: 20px;
+  background:rgba(255,255,255,0.05);
+}
+</style>

+ 12 - 0
admin/src/components/index.js

@@ -0,0 +1,12 @@
+/*
+ * @Author: Zp 导出需要全局注册的组件。
+ * @Date: 2020-03-20 09:54:19
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-03-20 10:23:56
+ */
+
+import tables from './tables'
+
+export default {
+  tables
+}

+ 187 - 0
admin/src/components/map/index.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="qqmap">
+    <div class="map-tools">
+      <!-- <Select class="map-select" v-model="form.city" size="large">
+        <Option value="1">12</Option>
+      </Select> -->
+      <!-- <Input class="search-input" v-model="form.keyword" size="large" search  enter-button @on-change="onChange" /> -->
+    </div>
+    <div class="map" id="map_container"></div>
+  </div>
+</template>
+
+<script>
+import { getDistrict, getLocationByIp, getLocationByGeocoder } from 'api/qqmap'
+export default {
+  props: {
+    city: String,
+    latitude: [Number, String],
+    longitude: [Number, String]
+  },
+  data () {
+    return {
+      form: {
+        keyword: ''
+      },
+      location: {},
+      map: null,
+      markerArr: [],
+      searchList: {}
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initMap()
+    })
+  },
+  methods: {
+    async initMap() {
+      await this.onChange()
+      await this.getLocationByIp()
+      let center = new TMap.LatLng(this.location.location.lat, this.location.location.lng);
+        //定义map变量,调用 TMap.Map() 构造函数创建地图
+        this.map = new TMap.Map(document.getElementById('map_container'), {
+          center: center, //设置地图中心点坐标
+          zoom: 17, //设置地图缩放级别
+        });
+        this.initMarkerEvent()
+      
+    },
+    getLocationByIp () {
+      if (this.location.location) {
+        return
+      }
+      return getLocationByIp().then(res => {
+        this.location = res.result
+      })
+    },
+    onChange (e) {
+      const keyword = this.city
+      if (this.latitude && this.longitude) {
+        return this.location = {
+          location: {
+            lat: this.latitude,
+            lng: this.longitude
+          }
+        }
+      }
+      return getDistrict(keyword).then(res => {
+        this.searchList = res.result[0]
+        if (this.searchList) {
+          this.location = {
+            location: {
+              lat: this.searchList[0].location.lat,
+              lng: this.searchList[0].location.lng
+            }
+          }
+        }
+      })
+      
+    },
+    initMarkerEvent () {
+      let marker = new TMap.MultiMarker({
+            id: 'marker-layer',
+            map: this.map,
+            styles: {
+                "marker": new TMap.MarkerStyle({
+                    "width": 25,
+                    "height": 35,
+                    "anchor": { x: 16, y: 32 },
+                    "src": 'https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png'
+                })
+            },
+            geometries: this.markerArr
+        })
+        marker.remove(["1"])
+        let latlng = {
+              lat: this.latitude,
+              lng: this.longitude
+            }
+        this.markerArr = [{
+          id: '1',
+          styleId: 'marker',
+          position: latlng,
+          properties: {
+            title: '1'
+          }
+        }]
+        marker.add(this.markerArr)
+        this.map.on('click', (evt) => {
+          marker.remove(["1"])
+          const latLng = evt.latLng
+          this.markerArr = [{
+            id: '1',
+            styleId: 'marker',
+            position: latLng,
+            properties: {
+              title: '1'
+            }
+          }]
+          marker.add(this.markerArr)
+          getLocationByGeocoder(`${latLng.lat},${latLng.lng}`).then(res => {
+            this.$emit('clickMap', {address_component: res.result.address_component,address: res.result.formatted_addresses.recommend, location: res.result.location})
+          })
+          
+        })
+    }
+  }
+};
+</script>
+
+<style lang="less">
+.qqmap {
+  position: relative;
+  width: 800px;
+  height: 400px;
+  background-color: #28272a;
+  padding-top: 20px;
+}
+.map-tools {
+  position: absolute;
+  top: 20px;
+  left: 20px;
+}
+.search-input {
+  z-index: 10000;
+  background: #fff;
+  width: 300px;
+  position: absolute;
+  left: 110px;
+  top: 0;
+  .ivu-input {
+    background-color: #fff;
+    color: #515a6e;
+    border: 1px solid #dcdee2;
+    z-index: 10000;
+  }
+  
+}
+.map {
+    position: relative;
+    width: 800px;
+  height: 400px;
+  }
+.map-select {
+  display: inline-block;
+  width: 100px;
+  .ivu-select-selection {
+    background-color: #fff;
+    color: #515a6e;
+    border: 1px solid #dcdee2;
+    z-index: 10000;
+  }
+  .ivu-select-dropdown {
+    background-color: #fff;
+    z-index: 10000;
+    
+  }
+  .ivu-select-item {
+    color: #515a6e;
+    z-index: 10000;
+    background-color: #fff;
+    &:hover {
+      background-color: #fff;
+    }
+  }
+}
+</style>

+ 96 - 0
admin/src/components/tables/handle-btns.vue

@@ -0,0 +1,96 @@
+<template>
+  <div>
+    <template v-for="(i, index) in toolsBtnGroup">
+      <Poptip
+        v-if="i == 'del'"
+        :key="index"
+        confirm
+        transfer
+        title="确定删除此数据吗?"
+        @on-ok="dispatchHandle(toolsBtnHandle[i], row)"
+      >
+        <span :class="`btn btn-${i}`">{{ btn_text[i] }}</span>
+      </Poptip>
+      <span :class="`btn btn-${i}`" v-else @click="dispatchHandle(toolsBtnHandle[i], row)">{{ btn_text[i] }}</span>
+    </template>
+  </div>
+</template>
+
+<script>
+import { userSave, userAccredit, userFindAuth, houseDel } from 'api'
+
+export default {
+  name: 'HandleBtns',
+  props: {
+    // 工具按钮组
+    toolsBtnGroup: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    row: {
+      type: Object,
+      default() {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      // 工具按钮事件
+      toolsBtnHandle: {
+        audit: 'toAuditLayout',
+        make: 'toMakeLayout',
+        view: 'toViewLayout',
+        author: 'toAuthorLayout',
+        edit: 'toEdit',
+        del: 'toDel'
+      },
+      btn_text: {
+        edit: '编辑',
+        del: '删除'
+      }
+    }
+  },
+  mounted() {
+  },
+  methods: {
+    // 派遣任务
+    dispatchHandle(action, row) {
+      this.$emit('handleClickBtn', {action, row})
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+.input-item {
+  margin-bottom: 20px;
+  .input-label,
+  .input {
+    padding: 5px 0;
+    display: inline-block;
+    vertical-align: middle;
+  }
+  .input-label {
+    width: 65px;
+  }
+  .input {
+    width: 310px;
+  }
+}
+.btn {
+  cursor: pointer;
+}
+.btn-edit {
+  color: #1FE4DC;
+  margin-right: 15px;
+  &:last-child {
+    margin-right: 0;
+  }
+}
+.btn-del {
+  color: #F56C6C;
+}
+</style>

+ 2 - 0
admin/src/components/tables/index.js

@@ -0,0 +1,2 @@
+import tables from './tables.vue'
+export default tables

+ 264 - 0
admin/src/components/tables/tables.vue

@@ -0,0 +1,264 @@
+<template>
+  <div class="table-main">
+    <div v-if="controllable" class="table-tools">
+      <div class=" clearfix">
+        <div class="tools-left fl">
+          <div class="tools-search-item" v-for="(tools, index) in tools" :key="index">
+            <label>{{ tools.label }}</label>
+            <Select clearable  v-model="pageParam[`tools_${index}`]" style="width:240px" size="large" :placeholder="tools.placeholder" filterable @on-change="handleTableData">
+              <Option v-for="item in tools.options" :value="item.value" :key="item.value">{{ item.label }}</Option>
+            </Select>
+          </div>
+          <div class="tools-search-item">
+            <Input :placeholder="placeholder" v-model="pageParam.searchKey" style="width: 240px" size="large" @on-enter="handleTableData" @on-click="handleTableData">
+                <Icon type="ios-search" slot="suffix" />
+            </Input>
+          </div>
+        </div>
+        <!-- 顶部按钮组,由父组件:buttonList 传递 -->
+      <div v-if="buttonList.length > 0" class="button-group fr">
+        <Button
+          v-for="(btn, index) in buttonList"
+          :key="index"
+          size="large"
+          :type="btn.type ? btn.type : 'primary'"
+          @click="btnHandle(btn.handle, btn.param)"
+        >{{ btn.text }}</Button>
+      </div>
+      </div>
+      
+    </div>
+    <Table :data="tableData" :columns="tableColumns" @on-selection-change="handleSelect">
+      <template slot="action" slot-scope="{ row, index }">
+        <handleBtns
+          :tools-btn-group="toolsBtnGroup"
+          :row="row"
+          @handleClickBtn="handleRowBtn"
+        />
+      </template>
+    </Table>
+    <div style="margin-top: 20px; overflow: hidden;">
+      <div style="float: right;">
+        <Page
+          v-if="pageParam.total > 0"
+          :current.sync="pageParam.current"
+          :total="pageParam.total"
+          :page-size="pageParam.page_size"
+          class-name="tatle-page"
+          size="small"
+          show-total
+          show-elevator
+          @on-change="changePage"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import handleBtns from './handle-btns.vue'
+
+export default {
+  name: 'Tables',
+  components: {
+    handleBtns
+  },
+  props: {
+    // 从父组件得到的数据接口
+    dataApi: Function,
+    deleteApi: Function,
+    deleteIdKey: String,
+    // 从父组件得到的表格列数据
+    columns: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    tools: {
+      type: Array,
+      default () {
+        return []
+      }
+    },
+    // 是否显示工具控件
+    controllable: {
+      type: Boolean,
+      default: true
+    },
+    // 是否显示按钮组
+    buttonList: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    long: {
+      type: Boolean,
+      default: false
+    },
+    placeholder: {
+      type: String,
+      default: '房源ID/名称/经纪人'
+    }
+  },
+  data() {
+    return {
+      // 通过dataApi获取,处理后的单元格数据
+      tableData: [],
+      // 通过props获取后,处理后的表格列数据
+      tableColumns: [],
+      // 工具按钮组
+      toolsBtnGroup: [],
+      time: null,
+      pageParam: { page_num: 1, page_size: 10, searchKey: '', total: 0, current: 1, tools_0: '', tools_1: '' }
+    }
+  },
+  computed: {
+    access() {
+      return this.$store.state.user.access
+    }
+  },
+  watch: {
+    // 监听搜索框,并在用户在时间间隔内只发送一次请求
+    'pageParam.searchKey'() {
+      this.$bebounce(this.handleTableData, { page_num: 1, current: 1 })
+    },
+    columns() {
+      this.handleTableData()
+    }
+  },
+  created() {
+  },
+  mounted() {
+    this.handleColumns(this.columns)
+    if (this.long) {
+      this.time = window.setInterval(() => {
+        setTimeout(this.handleTableData, 0)
+      }, 3000)
+    }
+    this.handleTableData()
+  },
+  beforeDestroy() {
+    clearInterval(this.time)
+  },
+  methods: {
+    // 处理表格列数据
+    handleColumns(columns) {
+      this.tableColumns = columns.map(item => {
+        const res = item
+        // 当表格列数据存在 slot 时,特殊处理
+        if (item.slot === 'action') this.slotAction(res)
+        return res
+      })
+    },
+    // 处理表格列中 含有 slot 的项
+    slotAction(item) {
+      const arr = ['admin', 'edit']
+      const access = this.access.some(i => arr.includes(i))
+      const { tools = [] } = item
+      // this.toolsBtnGroup = access ? tools : tools.filter(item => item == 'view' || item == 'del')
+      this.toolsBtnGroup = tools
+    },
+    // 处理单元格数据
+    handleTableData(param) {
+      const fetchParam = Object.assign(this.pageParam, param)
+      this.dataApi(fetchParam).then(
+        res => {
+          const { data } = res
+          const list = data.list || data
+          // list.forEach(item => {
+          //   if (item.roleKey === 'admin') item._disabled = true
+          //   item.orientation = this.oriAction(item.orientation)
+          // })
+          this.tableData = list
+          this.pageParam.total = data.totalNum || data.total
+        }
+      )
+    },
+    // 处理表格列中 朝向 的项
+    oriAction(ori) {
+      const aspectList = [
+        {
+          value: 'E',
+          label: '东'
+        },
+        {
+          value: 'W',
+          label: '西'
+        },
+        {
+          value: 'S',
+          label: '南'
+        },
+        {
+          value: 'N',
+          label: '北'
+        },
+        {
+          value: 'ES',
+          label: '东南'
+        },
+        {
+          value: 'EN',
+          label: '东北'
+        },
+        {
+          value: 'WS',
+          label: '西南'
+        },
+        {
+          value: 'WN',
+          label: '西北'
+        }
+      ]
+      for (let i = 0; i < aspectList.length; i++) {
+        if (aspectList[i].value === ori) {
+          return aspectList[i].label
+        }
+      }
+    },
+    // 选中某列 返回选中的数据
+    handleSelect(selection) {
+      this.$emit('chooseSelection', selection)
+    },
+    // 按钮事件
+    btnHandle(handleName, param) {
+      
+      this.$emit(handleName, param)
+    },
+    async handleRowBtn(param) {
+      if (param.action === 'toDel') {
+        // 获取对应的id字段得值
+        await this.deleteApi([param.row[this.deleteIdKey] || param.row[Object.keys(param.row).find(item => item.indexOf('_id') > -1)]])
+        this.handleTableData()
+        return
+      }
+      this.$emit(param.action, param.row)
+    },
+    // 分页
+    changePage(page) {
+      this.handleTableData({ page_num: page })
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+.search-box {
+  display: inline-block;
+}
+.button-group {
+  float: right;
+}
+.table-tools {
+  margin-bottom: 20px;
+}
+.tools-search-item {
+  margin-right: 20px;
+  display: inline-block;
+  label {
+    margin-right: 10px;
+  }
+}
+</style>

+ 134 - 0
admin/src/components/upload/index.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="upload-box">
+    <Upload
+      class="house-upload"
+      :multiple="multiple"
+      type="drag"
+      accept="image/png, image/jpeg"
+      :before-upload="handleBeforeUpload"
+      action="#"
+      :show-upload-list="false"
+      :data="data"
+    >
+      <div class="upload">
+        <Icon type="md-add" size="40" style="color: #1FE4DC;" />
+        <p>本地上传</p>
+      </div>
+    </Upload>
+      <div
+        v-for="(uploadfiles) in hasUploads"
+        :key="uploadfiles.img"
+        class="file-item"
+      >
+        <img :src="uploadfiles.img" />
+        <div
+          class="close-btn"
+          @click="handleCancleClick(uploadfiles, 'hasUploads')"
+        >
+          <Icon class="close" custom="iconfont iconform_close" />
+        </div>
+        <div
+          class="tip "
+          :class="{ 'is-cover': cover_image === uploadfiles.img }"
+          @click="handleClickCover(uploadfiles)"
+          v-if="cover_image !== undefined"
+        >
+          {{ cover_image === uploadfiles.img ? "封面图" : "设为封面图" }}
+        </div>
+      </div>
+      <div
+        v-for="(uploadfiles) in preUploads"
+        :key="uploadfiles.img"
+        class="file-item"
+      >
+        <img :src="uploadfiles.img" />
+        <div
+          class="close-btn"
+          @click="handleCancleClick(uploadfiles, 'preUploads')"
+        >
+          <Icon class="close" custom="iconfont iconform_close" />
+        </div>
+        <div
+          class="tip "
+          :class="{ 'is-cover': cover_image === uploadfiles.img }"
+          @click="handleClickCover(uploadfiles)"
+          v-if="cover_image !== undefined"
+        >
+          {{ cover_image === uploadfiles.img ? "封面图" : "设为封面图" }}
+        </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { uploadFile } from 'api/upload'
+export default {
+  props: {
+    preUploads: Array,
+    hasUploads: Array,
+    cover_image: String,
+    limit: Number,
+    multiple: {
+      type: Boolean,
+      default: true
+    },
+    data: {}
+  },
+  data () {
+    return {
+    }
+  },
+  computed: {
+    uploads () {
+      return [...this.hasUploads, ...this.preUploads]
+    }
+  },
+  methods: {
+    handleBeforeUpload (file) {
+      if (this.limit && this.uploads.length >= this.limit) {
+        this.$Message.error({
+          content: `只能上传${this.limit}张图片`
+        })
+        return
+      }
+      const reader = new FileReader()
+      reader.readAsDataURL(file)
+      reader.onload = () => {
+        const _base64 = reader.result
+        file.img = _base64
+        this.preUploads.push(file)
+      }
+      return false
+    },
+    handleCancleClick (uploadfile, type) {
+      this[type].splice(this[type].indexOf(uploadfile), 1)
+    },
+    uploadfiles (opts) {
+      if (opts) {
+        this.data = opts
+      }
+      console.log(opts, 'opts')
+      return Promise.all(this.preUploads.map(item => this.uploadTypeFile(item))).then(() => {
+        return this.cover_image
+      })
+    },
+    uploadTypeFile (file) {
+      return uploadFile(file, this.data).then(res => {
+        if (this.cover_image === file.img) {
+          this.handleClickCover({
+            img: res.data.url,
+            isUploaded: true
+          })
+        }
+        file.img = res.data.url
+        this.hasUploads.push(file)
+        this.preUploads.splice(this.preUploads.indexOf(file), 1)
+        return res
+      })
+    },
+    handleClickCover (file) {
+      this.$emit('changeCoverImage', file)
+    }
+  }
+}
+</script>

+ 125 - 0
admin/src/components/upload/video.vue

@@ -0,0 +1,125 @@
+<template>
+  <div class="upload-box">
+    <Upload
+      class="house-upload"
+      :multiple="multiple"
+      type="drag"
+      accept=".mp4,.mov"
+      :before-upload="handleBeforeUpload"
+      action="#"
+      :show-upload-list="false"
+    >
+      <div class="upload">
+        <Icon type="md-add" size="40" style="color: #1FE4DC;" />
+        <p>本地上传</p>
+      </div>
+    </Upload>
+      <div
+        v-for="(uploadfiles) in hasUploads"
+        :key="uploadfiles.img"
+        class="file-item"
+      >
+        <img :src="uploadfiles.img" />
+        <div
+          class="close-btn"
+          @click="handleCancleClick(uploadfiles, 'hasUploads')"
+        >
+          <Icon class="close" custom="iconfont iconform_close" />
+        </div>
+      </div>
+      <div
+        v-for="(uploadfiles) in preUploads"
+        :key="uploadfiles.img"
+        class="file-item"
+      >
+        <img :src="uploadfiles.img" />
+        <div
+          class="close-btn"
+          @click="handleCancleClick(uploadfiles, 'preUploads')"
+        >
+          <Icon class="close" custom="iconfont iconform_close" />
+        </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { uploadFile } from 'api/upload'
+export default {
+  props: {
+    limit: Number,
+    preUploads: Array,
+    hasUploads: Array,
+    multiple: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data () {
+    return {
+    }
+  },
+  computed: {
+    uploads () {
+      return [...this.hasUploads, ...this.preUploads]
+    }
+  },
+  methods: {
+    handleBeforeUpload (file) {
+      if (this.limit && this.uploads.length >= this.limit) {
+        this.$Message.error({
+          content: '上传数量已超过限制'
+        })
+        return
+      }
+      const url = URL.createObjectURL(file)
+      this.getVideoBase64(url).then(res => {
+        file.img = res
+        this.preUploads.push(file)
+        URL.revokeObjectURL(url)
+      })
+      return false
+    },
+    getVideoBase64(url) {
+        return new Promise(function (resolve, reject) {
+          let dataURL = '';
+          let video = document.createElement("video");
+          video.setAttribute('crossOrigin', 'anonymous');//处理跨域
+          video.setAttribute('src', url);
+          video.setAttribute('width', 100);
+          video.setAttribute('height', 100);
+          video.setAttribute('autoplay', true);
+          video.addEventListener('loadeddata', function () {
+              let canvas = document.createElement("canvas"),
+                  width = video.width, //canvas的尺寸和图片一样
+                  height = video.height;
+              canvas.width = width;
+              canvas.height = height;
+              canvas.getContext("2d").drawImage(video, 0, 0, width, height)
+              canvas.getContext("2d").drawImage(video, 0, 0, width, height); //绘制canvas
+              dataURL = canvas.toDataURL('image/png'); //转换为base64
+              resolve(dataURL);
+          });
+        })
+    },
+    handleCancleClick (uploadfile, type) {
+      this[type].splice(this[type].indexOf(uploadfile), 1)
+    },
+    uploadfiles () {
+      return Promise.all(this.preUploads.map(item => this.uploadTypeFile(item))).then((res) => {
+        return res
+      })
+    },
+    uploadTypeFile (file) {
+      return uploadFile(file).then(res => {
+        console.log(res.data, res.data.image)
+        file.video = res.data.video
+        file.cover_image = res.data.image
+        this.hasUploads.push(file)
+        this.preUploads.splice(this.preUploads.indexOf(file), 1)
+        return res
+      })
+    }
+  }
+}
+</script>

+ 20 - 0
admin/src/config/agency.js

@@ -0,0 +1,20 @@
+export const agency_type = [
+  {
+    label: '钻石经纪人',
+    value: '钻石经纪人'
+  },
+  {
+    label: '金牌经纪人',
+    value: '金牌经纪人'
+  },
+  {
+    label: '资深经纪人',
+    value: '资深经纪人'
+  }
+]
+
+export const agency_icon = {
+  '钻石经纪人': 'http://houseoss.4dkankan.com/4dHouse/admin/icon/diamond.svg',
+  '金牌经纪人': 'http://houseoss.4dkankan.com/4dHouse/admin/icon/gold.svg',
+  '资深经纪人': 'http://houseoss.4dkankan.com/4dHouse/admin/icon/senior.svg',
+}

+ 0 - 0
admin/src/config/apiCodeMsg.js


+ 72 - 0
admin/src/config/house.js

@@ -0,0 +1,72 @@
+export const house_type = [
+  {
+    label: '新房',
+    value: '1'
+  },
+  {
+    label: '二手房',
+    value: '2'
+  }
+]
+
+export const house_type_map = {
+  1: '新房',
+  2: '二手房'
+}
+
+export const house_usages = [
+  {
+    label: '住宅',
+    value: '住宅'
+  },
+  {
+    label: '非住宅',
+    value: '非住宅'
+  }
+]
+
+export const building_type = [
+  {
+    label: '板楼',
+    value: '板楼'
+  },
+  {
+    label: '塔楼',
+    value: '塔楼'
+  },
+  {
+    label: '板塔合一',
+    value: '板塔合一'
+  },
+]
+
+export const remarks_type = [
+  {
+    label: '装修描述',
+    value: '装修描述'
+  },
+  {
+    label: '周边配套',
+    value: '周边配套'
+  },
+  {
+    label: '小区介绍',
+    value: '小区介绍'
+  },
+  {
+    label: '核心卖点',
+    value: '核心卖点'
+  },
+  {
+    label: '交通出行',
+    value: '交通出行'
+  },
+]
+
+export const sale_house_type = ['一房一卫','一房一厅一卫','两房一厅一卫','三房一厅一卫','三房两厅两卫','四房两厅两卫']
+
+export const property_type = ['居住物业', '商业物业', '工业物业', '政府类物业', '其他用途物业']
+
+export const house_tags = ['住宅', '公寓', '新盘首开', '低密居所', '绿化率高', '大型社区', '交通便利', '配套齐全']
+
+export const building_tags = ['云看房', '随时可看']

+ 10 - 0
admin/src/config/url.js

@@ -0,0 +1,10 @@
+// 当前环境变量
+const env = process.env.NODE_ENV
+
+// 开发环境 和 生产环境 的 根路径
+// const DEV_URL = 'http://192.168.0.135:8010/'
+// const DEV_URL = 'http://39.108.220.65:8084/fdkanfang/'
+const DEV_URL = '/admin'
+const PRO_URL = '/admin'
+
+export default env === 'development' ? DEV_URL : PRO_URL

+ 12 - 0
admin/src/directive/index.js

@@ -0,0 +1,12 @@
+/*
+ * @Author: Zp  指令系统,暂时还没用到,先简单配置下
+ * @Date: 2020-03-11 11:12:47
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-03-11 12:25:04
+ */
+
+export default Vue => {
+  Vue.directive('test', function() {
+    console.log('测试指令系统')
+  })
+}

+ 178 - 0
admin/src/layout/index/index.vue

@@ -0,0 +1,178 @@
+<template>
+  <Layout class="layout">
+
+    <Sider width="310">
+      
+      <div class="layout-logo">
+        看房4DKanKan-管理后台
+      </div>
+      <Menu theme="dark" :active-name="$route.name" width="auto" class="layout-menu">
+        <MenuGroup >
+          <template v-for="(item, index) in menuList[0].children">
+            <MenuItem
+              :key="item.name"
+              :to="item.path"
+              :name="item.name"
+              v-if="!item.children || item.redirect"
+            >
+              <div class="menu-item">
+                <Icon class="menu-icon" :custom="`iconfont icon${item.meta.icon}`" />
+                {{ item.meta.title }}
+              </div>
+            </MenuItem>
+            <Submenu :name="index" v-else>
+                <template slot="title">
+                    <Icon class="menu-icon" :custom="`iconfont icon${item.meta.icon}`" />
+                    {{ item.meta.title }}
+                </template>
+                <MenuItem :name="`${index}-${childIndex}`" :to="`${item.path}/${child.path}`" v-for="(child, childIndex) in item.children" :key="child.path">{{child.meta.title}}</MenuItem>
+            </Submenu>
+          </template>
+        </MenuGroup>
+      </Menu>
+    </Sider>
+    <Layout>
+      <Header class="layout-header">
+        <div class="header-user">
+          <span class="user-name">{{ userName }}</span>
+            <span class="user-btn" @click="changeEditShow(true)"><Icon custom="iconfont iconnav_password" /><span>修改密码</span></span>
+            <span class="user-btn" @click="logoutModal=true"><Icon custom="iconfont iconnav_exit" /><span>退出登录</span></span>
+            <i class="iconfont iconarrow_down" />
+        </div>
+      </Header>
+      <div class="layout-navigation">
+        <Breadcrumb separator=">">
+          <BreadcrumbItem class="bread-navigation" v-for="item in breadcrumb" :key="item">{{ item }}</BreadcrumbItem>
+        </Breadcrumb>
+      </div>
+      <Content class="layout-main">
+        <!-- <keep-alive>
+          <router-view v-if="!$route.meta.noKeepAlive" />
+        </keep-alive>
+        <router-view v-if="$route.meta.noKeepAlive" /> -->
+        <router-view />
+      </Content>
+    </Layout>
+    <Modal v-model="show" class="edit-modal" width="420" footer-hide>
+      <div class="modal-header" slot="header">修改密码</div>
+      <editPassword v-if="show" @change="changeEditShow" />
+    </Modal>
+    <CModal :show="logoutModal" :width="420" title="提示" @close="logoutModal=false" @submit="handleLogout" submitText="确定">
+      <div style="height:60px">确定退出登录吗?</div>
+    </CModal>
+  </Layout>
+</template>
+
+<script>
+import { mapActions } from 'vuex'
+import editPassword from 'components/editPassword'
+import CModal from 'components/Modal'
+export default {
+  name: 'Index',
+  data() {
+    return {
+      show: false,
+      logoutModal: false
+    }
+  },
+  computed: {
+    menuList() {
+      return this.$store.getters.menuList
+    },
+    userName() {
+      return this.$store.getters.userName
+    },
+    breadcrumb () {
+      return this.$route.matched.map(item => item.meta.title).filter(item => item)
+    }
+  },
+  components: {
+    editPassword,
+    CModal
+  },
+  methods: {
+    ...mapActions([
+      'logout'
+    ]),
+    handleLogout() {
+      this.logout().then(() => {
+        this.$router.push({
+          name: 'login'
+        })
+      })
+    },
+    changeEditShow (show) {
+      this.show = show
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+.layout {
+  height: 100%;
+  &-logo {
+    position: relative;
+    height: 64px;
+    border-bottom: 1px solid #555a5a;
+    line-height: 64px;
+    padding-left: 23px;
+    font-size: 16px;
+  }
+
+  &-menu {
+    .menu-item {
+      height: 28px;
+      line-height: 28px;
+      .menu-icon {
+        margin-right: 10px;
+      }
+    }
+  }
+
+  &-header {
+    border-bottom: 1px solid #555a5a;
+    height: 64px;
+    .header-user {
+      float: right;
+      // cursor: pointer;
+      color: #fff;
+      .user-name {
+        display: inline-block;
+        margin-right: 10px;
+        vertical-align: middle;
+      }
+    }
+    .header-user /deep/ .ivu-select-dropdown {
+      background-color: #161a1a;
+      .ivu-dropdown-item:hover {
+        background-color: #161a1a;
+      }
+    }
+  }
+
+  &-navigation {
+    height: 48px;
+    padding: 12px 29px;
+    border-left: 1px solid #555a5a;
+    background-color: #252828;
+  }
+  .bread-navigation {
+    color: rgba(255, 255, 255, 0.8);
+    font-weight: 400;
+  }
+
+  .ivu-menu-dark.ivu-menu-vertical
+    .ivu-menu-item-active:not(.ivu-menu-submenu) {
+    color: #fff;
+  }
+}
+.user-btn {
+  margin-left: 30px;
+  font-size: 14px;
+  cursor: pointer;
+  span {
+    margin-left: 8px;
+  }
+}
+</style>

+ 3 - 0
admin/src/layout/login/index.js

@@ -0,0 +1,3 @@
+
+import login from './login.vue'
+export default login

+ 135 - 0
admin/src/layout/login/login.vue

@@ -0,0 +1,135 @@
+<template>
+  <div class="login-account">
+    <div class="login-account-header">
+      <span class="header-text">好玩展-管理后台</span>
+    </div>
+    <div class="login-account-container">
+      <div class="login-3d-con">
+        <div class="login-banner">
+          <div class="login-container-text">
+            <div class="login-con-header">
+              <span>在线语音带看  足不出户看实况</span>
+            </div>
+            <div class="login-con-desc">
+              <span>让你的房产交易更加便捷</span>
+            </div>
+          </div>
+        </div>
+        <login-form />
+      </div>
+    </div>
+    <div class="login-account-footer">
+      <div class="footer-text">
+        <div>TEL: 4006698025 | FAX: 0756-6996790 | E-Mail:sales@4dage.com</div>
+        <div>
+          <img src="~assets/login/img_kor@2x.png">粤ICP备14078495号-5
+        </div>
+        <div>Copyright2018 Sitename. Design by 4Dage Technology Co.Ltd.</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import loginForm from 'components/LoginForm.vue'
+
+export default {
+  name: 'Login',
+  components: {
+    loginForm
+  },
+  data() {
+    return {}
+  }
+}
+</script>
+
+<style scoped lang="less">
+.login-account {
+  min-width: 1280px;
+  min-height: 100%;
+  background-color: #252828;
+  &-header {
+    display: flex;
+    height: 140px;
+    .header-text {
+      margin: auto;
+      font-size: 24px;
+      color: rgba(255, 255, 255, 0.88);
+      text-align: center;
+      font-weight: bold;
+    }
+  }
+  &-container {
+    width: 100%;
+    height: 600px;
+    background-color: rgba(0, 0, 0, 0.3);
+    background-image: url("~assets/login/img_loginele_points@2x.png");
+    background-repeat: no-repeat;
+    background-position: bottom;
+    background-size: 100%;
+    display: flex;
+    .login-3d-con {
+      width: 1240px;
+      margin: 96px auto;
+      display: flex;
+      align-items: center;
+      .login-banner {
+        position: relative;
+        width: 680px;
+        height: 408px;
+        margin-right: 218px;
+        background-repeat: no-repeat;
+        background-position: bottom;
+        background-size: 100%;
+        .login-container-text {
+          position: absolute;
+          top: 34px;
+          left: 2px;
+          color: rgba(255, 255, 255, 0.88);
+          .login-con-logo {
+            width: 148px;
+            height: 40px;
+          }
+          .login-con-header {
+            margin-top: 50px;
+            font-size: 48px;
+            font-weight: bold;
+            letter-spacing: 1px;
+          }
+          .login-con-desc {
+            margin-top: 10px;
+            font-size: 22px;
+            letter-spacing: 5px;
+          }
+        }
+        img {
+          width: 100%;
+          height: 100%;
+        }
+      }
+    }
+  }
+  &-footer {
+    overflow: hidden;
+    height: 140px;
+    background-color: #2c2c2c;
+    .footer-text {
+      width: 386px;
+      height: 68px;
+      margin: 36px auto;
+      text-align: center;
+      font-size: 12px;
+      line-height: 26px;
+      color: #fff;
+      opacity: 0.38;
+      img {
+        width: 18px;
+        height: 20px;
+        margin: 0 5px;
+        vertical-align: middle;
+      }
+    }
+  }
+}
+</style>

+ 9 - 0
admin/src/layout/makeLayout/index.js

@@ -0,0 +1,9 @@
+/*
+ * @Author: Zp 外部骨架部分,桥接视图、路由。
+ * @Date: 2020-02-25 11:24:49
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-03-19 14:19:13
+ */
+
+import makeLayout from './makeLayout.vue'
+export default makeLayout

+ 95 - 0
admin/src/layout/makeLayout/makeLayout.vue

@@ -0,0 +1,95 @@
+<template>
+  <Layout>
+    <Header class="make-layout-header">
+      <div class="edit-logo">
+        <img class="edit-logo-img" src="~assets/logo/img_edit_logo@2x.png" alt>
+      </div>
+      <div class="edit-title">
+        <p class="title-text">{{ data.num }} {{ data.districtName }} {{ data.unitType }}</p>
+      </div>
+      <div class="edit-exit" @click="exitEdit">
+        退出编辑
+        <Icon custom="iconfont iconedit_exit" />
+      </div>
+    </Header>
+    <Content>
+      <router-view :data="data" @change-floor="getHouseDetail" />
+    </Content>
+  </Layout>
+</template>
+
+<script>
+import { houseDetail } from 'api'
+
+export default {
+  name: 'MakeLayout',
+  data() {
+    return {
+      id: this.$route.params.id,
+      data: {}
+    }
+  },
+  computed: {
+  },
+  watch: {
+    '$route'() {
+      this.getHouseDetail()
+    }
+  },
+  mounted() {
+    this.getHouseDetail()
+  },
+  methods: {
+    getHouseDetail(floor = '1') {
+      houseDetail({ id: this.id, floor }).then(
+        res => {
+          this.data = res.data
+          this.computedUnitType()
+        }
+      )
+    },
+    computedUnitType() {
+      const _unitType = this.data.unitType.split('-')
+      this.data.unitType = `${_unitType[0]}房${_unitType[1]}厅${_unitType[2]}卫`
+    },
+    exitEdit() {
+      sessionStorage.clear()
+      this.$router.push('/edit')
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+.make-layout-header {
+  display: flex;
+  border-bottom: 1px solid #555;
+}
+
+.edit-logo {
+  display: inline-block;
+  position: relative;
+  height: 50px;
+  &-img {
+    width: 104px;
+    height: 20px;
+    vertical-align: middle;
+  }
+}
+
+.edit-title {
+  display: inline-block;
+  overflow: hidden;
+  flex: 1;
+  .title-text {
+    font-size: 16px;
+    color: rgba(255, 255, 255, 0.88);
+    text-align: center;
+  }
+}
+
+.edit-exit {
+  cursor: pointer;
+  float: right;
+}
+</style>

+ 71 - 0
admin/src/libs/request.js

@@ -0,0 +1,71 @@
+import Axios from 'axios'
+import { Message } from 'view-design'
+
+import { getToken, setToken, getAdmin } from 'libs/token'
+
+// 根路径
+import baseURL from 'config/url'
+
+const service = Axios.create({
+  baseURL,
+  // withCredentials: true, // 当跨域请求时发送cookie
+  timeout: 100000
+})
+
+// 保存调用栈
+// let time = null
+
+// 设置请求拦截器,每个请求都会到这里
+service.interceptors.request.use(
+  config => {
+    if (getToken()) {
+      // 给每个请求都加上后台需要的信息,可以自定义
+      config.params = Object.assign({
+        token: getToken(),
+        user_id: getAdmin().admin_id
+      }, config.params || {})
+    }
+    
+    // 在发送请求之前还能做点东西
+
+    // 拦截器函数, 要求返回config参数
+    return config
+  }, error => {
+    // 当报错的时候可以打印日志
+    console.log(error)
+    return Promise.reject(error)
+  }
+)
+
+// 设置响应拦截器,每个响应都会到这里
+service.interceptors.response.use(
+  response => {
+    // 当获得响应数据,可以根据响应的状态码判断后台状态
+    const { data } = response
+    // 处理后端响应数据状态码不在预设值得情况下,处理异常,这里预设值为2000
+    if (data.code && data.code != 0) {
+      // 再进一步判断
+      if (data.code == 3004 || data.code == 3005) {
+        data.code == 3005 && Message.error({ content: 'token过期,请重新登录' })
+        app.$router.push('/login')
+        setToken('')
+      } else {
+        // 非特定情况
+        if (data.msg) Message.error({ content: data.msg, background: true })
+      }
+      // 抛出错误信息
+      return Promise.reject(data)
+    } else {
+      return Promise.resolve(data)
+    }
+  }, error => {
+    console.log('err' + error) // 帮助debug
+    // Message.error({
+    //   content: '接口报错了,请稍后重试~',
+    //   background: true
+    // })
+    return Promise.reject(error)
+  }
+)
+
+export default service

+ 53 - 0
admin/src/libs/token.js

@@ -0,0 +1,53 @@
+/*
+ * @Author: Zp 管理登录令牌、cookie
+ * @Date: 2020-03-12 11:42:19
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-03-30 09:40:49
+ */
+
+import Cookie from 'js-cookie'
+
+// 设置存储 cookie 的键值
+const TokenKey = 'Authorization'
+
+// Token
+export function getToken() {
+  return Cookie.get(TokenKey)
+}
+
+export function setToken(token) {
+  return Cookie.set(TokenKey, token)
+}
+
+export function removeToken() {
+  return Cookie.remove(TokenKey)
+}
+
+// 用户名
+export function getUserName() {
+  return Cookie.get('userName')
+}
+
+export function setUserName(userName) {
+  return Cookie.set('userName', userName)
+}
+
+// 权限列表
+export function getRoleList() {
+  return Cookie.get('roleList')
+}
+
+export function setRoleList(roleList) {
+  return Cookie.set('roleList', roleList)
+}
+
+const AdminsKey = 'admin_key'
+
+export function setAdmin (data) {
+  return Cookie.set(AdminsKey, JSON.stringify(data))
+}
+
+export function getAdmin () {
+  const admin = Cookie.get(AdminsKey)
+  return admin ? JSON.parse(admin) : ''
+}

+ 160 - 0
admin/src/libs/tools.js

@@ -0,0 +1,160 @@
+/*
+ * @Author: Zp 工具函数
+ * @Date: 2020-03-12 16:14:38
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-04-15 16:59:40
+ */
+
+import routes from 'router/routes'
+
+// 公用工具函数 start
+
+/**
+ * @description 判断要查询的数组是否至少有一个元素包含在目标数组中
+ * @param {Array} target 目标数组
+ * @param {Array} arr 需要查询的数组
+ */
+export const hasOneOf = (targetarr, arr) => {
+  return targetarr.some(_ => arr.indexOf(_) > -1)
+}
+
+/**
+ * @description 抽象循环函数
+ * @param {Array} arr 循环的数组
+ * @param {function} fn 循环调用的函数
+ */
+export const forEach = (arr, fn) => {
+  if (!arr.length || !fn) return
+  let i = -1
+  const len = arr.length
+  while (++i < len) {
+    const item = arr[i]
+    fn(item, i, arr)
+  }
+}
+
+/**
+ * @description 实现对象的深拷贝,包括循环引用
+ * @param {Object|Array} target 需要深拷贝的对象
+ * @param {WeakMap} map 弱引用的键值对集合,可以手动将引用对象=null,回收内存
+ */
+export const deepClone = (target, map = new WeakMap()) => {
+  if (typeof target === 'object') {
+    // 先浅拷贝目标对象,无论对象还是数组一律按对象处理
+    const clone = Object.assign({}, target)
+
+    // 解决循环引用
+    if (map.get(target)) return map.get(target)
+    map.set(target, clone)
+    // 解决循环引用
+
+    Object.keys(clone).forEach(key => deepClone(target[key]), map)
+
+    return Array.isArray(target) ? Array.from(clone) : clone
+  } else {
+    return target
+  }
+}
+
+export function getT() {}
+
+/**
+ * @description 判断item是否有children数组length并且不为0
+ * @param {Object} item
+ */
+export const hasChild = item => {
+  return item.children && item.children.length !== 0
+}
+
+/**
+ * @description 根据路由上的meta和需要访问的权限判断是否能显示该菜单
+ * @param {Object} item 当前路由源对象
+ * @param {String} access 当前路由需要的访问权限 ['admin']
+ */
+const showThisMenuEle = (item, access) => {
+  if (item.meta && item.meta.access && item.meta.access.length) {
+    if (hasOneOf(item.meta.access, access)) return true
+    else return false
+  } else return true
+}
+
+export const localRead = (key) => {
+  return localStorage.getItem(key) || ''
+}
+
+// 公用工具函数 end
+
+// 权限控制工具函数 start
+
+/**
+ * @param {*} access 用户权限数组,如 ['super_admin', 'admin']
+ * @param {*} route 路由列表
+ */
+const hasAccess = (access, route) => {
+  if (route.meta && route.meta.access) return hasOneOf(access, route.meta.access)
+  else return true
+}
+
+/**
+ * 权鉴函数
+ * @description 用户是否可跳转到该页
+ * @param {*} name 即将跳转的路由name
+ * @param {*} access 用户权限数组
+ * @param {*} routes 路由列表
+ */
+export const canTurnTo = (name, access, routes) => {
+  const routePermissionJudge = (list) => {
+    return list.some(item => {
+      if (item.children && item.children.length) {
+        return routePermissionJudge(item.children)
+      } else if (item.name === name) {
+        return hasAccess(access, item)
+      }
+    })
+  }
+
+  return routePermissionJudge(routes)
+}
+
+/**
+ * 跳转函数
+ * @param {*} to 即将跳转的路由对象
+ * @param {*} access 用户权限数组
+ * @param {*} next 跳转函数
+ */
+export const turnTo = (to, access, next) => {
+  if (canTurnTo(to.name, access, routes)) next() // 有权限,可访问
+  else next({ replace: true, name: 'login' }) // 无权限,重定向到无权限访问页面
+}
+
+// 权限控制工具函数 end
+
+// 其他工具函数 start
+
+/**
+ * @description 根据路由获取侧边栏菜单
+ * @param {Array} list 通过路由列表得到菜单列表
+ * @returns {Array}
+ */
+export const getMenuByRouter = (list, access) => {
+  const res = []
+  forEach(list, item => {
+    if (!item.meta || (item.meta && !item.meta.hideInMenu)) {
+      const obj = {
+        icon: (item.meta && item.meta.icon) || '',
+        name: item.name,
+        meta: item.meta,
+        path: item.path,
+        redirect: item.redirect
+      }
+      if ((hasChild(item) || (item.meta && item.meta.showAlways)) && showThisMenuEle(item, access)) {
+        obj.children = getMenuByRouter(item.children, access)
+      }
+      if (item.meta && item.meta.href) obj.href = item.meta.href
+      if (showThisMenuEle(item, access)) res.push(obj)
+    }
+  })
+  return res
+}
+
+// 其他工具函数 end

+ 35 - 0
admin/src/locale/index.js

@@ -0,0 +1,35 @@
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+import { localRead } from 'libs/tools'
+// import zhCN from './zh-CN'
+import zhCnLocale from 'view-design/src/locale/lang/zh-CN'
+import enUsLocale from 'view-design/src/locale/lang/en-US'
+import zhTwLocale from 'view-design/src/locale/lang/zh-TW'
+
+Vue.use(VueI18n)
+
+// 自动根据浏览器系统语言设置语言
+const navLang = navigator.language
+const localLang = (navLang === 'zh-CN' || navLang === 'en-US') ? navLang : false
+const lang = localLang || localRead('local') || 'zh-CN'
+
+Vue.config.lang = lang
+
+// vue-i18n 6.x+写法
+Vue.locale = () => { }
+const messages = {
+  'zh-CN': Object.assign(zhCnLocale),
+  'zh-TW': Object.assign(zhTwLocale),
+  'en-US': Object.assign(enUsLocale)
+}
+const i18n = new VueI18n({
+  locale: lang,
+  messages
+})
+
+export default i18n
+
+// vue-i18n 5.x写法
+// Vue.locale('zh-CN', Object.assign(zhCnLocale, customZhCn))
+// Vue.locale('en-US', Object.assign(zhTwLocale, customZhTw))
+// Vue.locale('zh-TW', Object.assign(enUsLocale, customEnUs))

+ 109 - 0
admin/src/locale/zh-CN.js

@@ -0,0 +1,109 @@
+export default {
+  i: {
+    locale: 'zh-CN',
+    select: {
+      placeholder: '请选择',
+      noMatch: '无匹配数据',
+      loading: '加载中'
+    },
+    table: {
+      noDataText: '暂无数据',
+      noFilteredDataText: '暂无筛选结果',
+      confirmFilter: '筛选',
+      resetFilter: '重置',
+      clearFilter: '全部',
+      sumText: '合计'
+    },
+    datepicker: {
+      selectDate: '选择日期',
+      selectTime: '选择时间',
+      startTime: '开始时间',
+      endTime: '结束时间',
+      clear: '清空',
+      ok: '确定',
+      datePanelLabel: '[yyyy年] [m月]',
+      month: '月',
+      month1: '1 月',
+      month2: '2 月',
+      month3: '3 月',
+      month4: '4 月',
+      month5: '5 月',
+      month6: '6 月',
+      month7: '7 月',
+      month8: '8 月',
+      month9: '9 月',
+      month10: '10 月',
+      month11: '11 月',
+      month12: '12 月',
+      year: '年',
+      weekStartDay: '0',
+      weeks: {
+        sun: '日',
+        mon: '一',
+        tue: '二',
+        wed: '三',
+        thu: '四',
+        fri: '五',
+        sat: '六'
+      },
+      months: {
+        m1: '1月',
+        m2: '2月',
+        m3: '3月',
+        m4: '4月',
+        m5: '5月',
+        m6: '6月',
+        m7: '7月',
+        m8: '8月',
+        m9: '9月',
+        m10: '10月',
+        m11: '11月',
+        m12: '12月'
+      }
+    },
+    transfer: {
+      titles: {
+        source: '源列表',
+        target: '目的列表'
+      },
+      filterPlaceholder: '请输入搜索内容',
+      notFoundText: '列表为空'
+    },
+    modal: {
+      okText: '确定',
+      cancelText: '取消'
+    },
+    poptip: {
+      okText: '确定',
+      cancelText: '取消'
+    },
+    page: {
+      prev: '上一页',
+      next: '下一页',
+      total: '共',
+      item: '条',
+      items: '条',
+      prev5: '向前 5 页',
+      next5: '向后 5 页',
+      page: '条/页',
+      goto: '跳至',
+      p: '页'
+    },
+    rate: {
+      star: '星',
+      stars: '星'
+    },
+    time: {
+      before: '前',
+      after: '后',
+      just: '刚刚',
+      seconds: '秒',
+      minutes: '分钟',
+      hours: '小时',
+      days: '天'
+    },
+    tree: {
+      emptyText: '暂无数据'
+    }
+  }
+}

+ 35 - 0
admin/src/main.js

@@ -0,0 +1,35 @@
+// The Vue build version to load with the `import` command
+// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
+import Vue from 'vue'
+
+// 引入全局插件入口
+import installPlugin from 'plugin'
+
+// 引入注册指令方法
+import installDirective from 'directive'
+
+// 引入全局注册入口
+import registerCps from 'register'
+
+import App from './App'
+import router from './router'
+import store from './store'
+
+// 生产环境关掉提示
+Vue.config.productionTip = false
+
+// 注册指令
+installDirective(Vue)
+// 安装插件
+installPlugin(Vue)
+// 注册组件
+registerCps(Vue)
+
+/* eslint-disable no-new */
+window.app = new Vue({
+  el: '#app',
+  router,
+  store,
+  components: { App },
+  render: h => h(App)
+})

+ 34 - 0
admin/src/plugin/index.js

@@ -0,0 +1,34 @@
+import less from 'less'
+import ViewUI from 'view-design'
+import 'view-design/dist/styles/iview.css'
+import i18n from '@/locale'
+
+const plugin = {
+  less,
+  ViewUI,
+  // 配置自定义插件,bebouncePlugin可以是对象,也可以是函数,函数不能配置options参数
+  bebouncePlugin: {
+    install(Vue, options) {
+      // 防抖
+      Vue.prototype.$bebounce = function(fn) {
+        clearTimeout(this.fun)
+        this.fun = setTimeout(() => {
+          fn.call(this, arguments[1])
+        }, 500)
+      }
+    },
+    options: {
+      name: 'debouncePlugin'
+    }
+  }
+}
+
+export default Vue => {
+  Vue.use(ViewUI, {
+    i18n: (key, value) => i18n.t(key, value)
+  })
+  for (const name in plugin) {
+    const value = plugin[name]
+    Vue.use(value, typeof value === 'object' ? value : undefined)
+  }
+}

+ 25 - 0
admin/src/register/index.js

@@ -0,0 +1,25 @@
+/*
+ * @Author: Zp  统一注册全局组件
+ * @Desc: 导入全局组件,循环注册到全局
+ * @Date: 2020-03-11 11:12:47
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-03-20 10:45:50
+ */
+
+// import cps from 'components'
+// import { cp } from 'shelljs'
+
+export default Vue => {
+  // Object.keys(cps).forEach(key => {
+  //   // 规范式命名,比如驼峰式命名 editTable 将被转换成 edit-Table
+  //   // 还接受EditTable 的组件命名 -edit-Table -> edit-Table
+  //   // 再全部转换成小写
+  //   let cpName = key.replace(/([A-Z])/g, '-$1').toLowerCase()
+
+  //   if (cpName && cpName[0] === '-') {
+  //     cpName = cpName.replace('-', '')
+  //   }
+
+  //   Vue.component(cpName, cps[key])
+  // })
+}

+ 73 - 0
admin/src/router/index.js

@@ -0,0 +1,73 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+// import ViewUI from 'view-design'
+// import store from 'store'
+
+// import { getToken, setToken } from 'libs/token'
+// import { turnTo } from 'libs/tools'
+
+import routes from './routes'
+
+Vue.use(Router)
+
+const router = new Router({
+  // mode: 'history',
+  routes
+})
+
+// 定义登录页面路由name
+// const LOGIN_PAGE_NAME = 'login'
+// // 定义首页页面路由name
+// const GUIDE_PAGE_NAME = 'guide'
+
+// const needVerify = true
+
+// 添加路由钩子
+// router.beforeEach((to, form, next) => {
+//   ViewUI.LoadingBar.start()
+
+//   const token = getToken()
+//   next()
+//   if (!needVerify || to.name === 'test') {
+//     next()
+//   } else if (!token && to.name !== LOGIN_PAGE_NAME) {
+//     // 未登录且要跳转的页面不是登录页
+//     next({
+//       name: LOGIN_PAGE_NAME // 跳转到登录页
+//     })
+//   } else if (!token && to.name === LOGIN_PAGE_NAME) {
+//     // 未登录且要跳转登录页,直接跳转
+//     next()
+//   } else if (token && to.name === LOGIN_PAGE_NAME) {
+//     // 已登录且要跳转登录页,此时需要重定向到首页
+//     next({
+//       name: GUIDE_PAGE_NAME
+//     })
+//   } else {
+//     // 这里做鉴权处理
+//     // 从状态管理仓库获取登录用户信息,access必须是一个数组,如:['super_admin'] ['super_admin', 'admin']
+//     if (store.state.user.hasAccess) {
+//       turnTo(to, store.state.user.access, next)
+//     } else {
+//       // 拉取用户信息,通过用户权限和跳转的页面的name来判断是否有权限访问;
+//       store.dispatch('checkRole').then(user => {
+//         turnTo(to, user.access, next)
+//       }).catch(() => {
+//         setToken('')
+//         next({
+//           name: 'login'
+//         })
+//       })
+
+//       store.dispatch('userGetRole')
+//     }
+//   }
+// })
+
+// router.afterEach(
+//   to => {
+//     ViewUI.LoadingBar.finish()
+//   }
+// )
+
+export default router

+ 88 - 0
admin/src/router/routes.js

@@ -0,0 +1,88 @@
+import Index from 'layout/index/index'
+export default [
+  {
+    path: '/login',
+    name: 'login',
+    meta: {
+      title: '登录',
+      hideInMenu: true
+    },
+    // 登录界面
+    component: resolve => require(['layout/login/login'], resolve) 
+  },
+  {
+    path: '/',
+    name: 'layout',
+    redirect: '/enterprise',
+    meta: {
+      title: '',
+      hideInMenu: false
+    },
+    // 外层视图layout
+    component: Index,
+    children: [
+      {
+      path: '/enterprise',
+      name: 'enterprise',
+      meta: {
+        title: '企业信息',
+        icon: 'nav_enterprise'
+      },
+      component: resolve => require(['views/enterprise'], resolve) 
+    }, 
+    
+    {
+      path: '/system',
+      name: 'system',
+      meta: {
+        title: '系统管理',
+        icon: 'iconnav_sys',
+        // 不需要缓存路由组件
+        noKeepAlive: true
+        // access: ['admin']
+      },
+      // 用户管理
+      component: resolve => require(['views/system'], resolve),
+      children: [
+        {
+          path: 'user',
+          name: 'system',
+          meta: {
+            title: '用户管理',
+            noKeepAlive: true
+          },
+          component: resolve => require(['views/system/user/list'], resolve),
+        },
+        {
+          path: 'user/create',
+          name: 'system',
+          meta: {
+            title: '添加用户',
+            hideInMenu: true
+          },
+          component: resolve => require(['views/system/user/list'], resolve),
+        },
+        {
+          path: 'address',
+          name: 'system',
+          meta: {
+            title: '地址管理',
+            noKeepAlive: true
+          },
+          component: resolve => require(['views/system/address/list'], resolve),
+        },
+        {
+          path: 'store',
+          name: 'system',
+          meta: {
+            title: '门店管理',
+            noKeepAlive: true
+          },
+          component: resolve => require(['views/system/store/list'], resolve),
+        },
+        
+      ]
+    }
+  ]
+  }
+]

+ 23 - 0
admin/src/store/app.js

@@ -0,0 +1,23 @@
+/*
+ * @Author: Zp 应用模块 仓库 store
+ * @Desc: 这里是应用层级 主要存储应用级数据 例如导航菜单
+ * @Date: 2020-03-11 16:59:43
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-04-30 19:42:41
+ */
+
+import { getMenuByRouter } from 'libs/tools'
+import routers from 'router/routes'
+
+export default {
+  state: {
+  },
+  getters: {
+    menuList: (state, getters, rootState) => getMenuByRouter(routers, rootState.user.access),
+  },
+  mutations: {
+  },
+  actions: {
+  },
+  modules: {}
+}

+ 34 - 0
admin/src/store/index.js

@@ -0,0 +1,34 @@
+/*
+ * @Author: Zp store仓库,储存全局数据,分发任务
+ * @Date: 2020-03-05 10:48:41
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-04-16 13:57:29
+ */
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+// 按模块分类,分别管理,每个模块都是一个独立的store
+// 应用模块
+import app from './app'
+// 用户模块
+import user from './user'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {
+    // 添加测试用例数据
+    testData: 0
+  },
+  mutations: {
+    // 添加测试提交变更
+    testCommit(state, value) {
+      state.testData = value
+    }
+  },
+  actions: {},
+  modules: {
+    app,
+    user
+  }
+})

+ 101 - 0
admin/src/store/user.js

@@ -0,0 +1,101 @@
+/*
+ * @Author: Zp 用户模块 仓库 store
+ * @Desc: 这里存储用户数据、登录操作、权限查找、设置token等功能
+ * @Date: 2020-03-11 16:59:43
+ * @Last Modified by: Zp
+ * @Last Modified time: 2020-04-29 20:47:02
+ */
+import { login, checkRole, userGetRole } from 'api'
+import { getToken, setToken, getUserName, setUserName, getRoleList, setRoleList, setAdmin } from 'libs/token'
+
+export default {
+  state: {
+    token: getToken(),
+    userName: getUserName(),
+    roleList: getRoleList(),
+    // 可操作权限角色数组
+    access: [],
+    // 判断是否存入权限角色数组
+    hasAccess: false
+  },
+  getters: {
+    userName: state => state.userName,
+    access: state => state.access
+  },
+  mutations: {
+    // 设置角色名
+    setName(state, userName) {
+      state.userName = userName
+      setUserName(userName)
+    },
+    // 设置权限列表
+    setRoles(state, roleList) {
+      state.roleList = roleList
+      setRoleList(roleList)
+    },
+    // 设置权限角色数组
+    setAccess(state, access) {
+      state.access = access
+    },
+    // 设置写入权限角色数组状态
+    setHasAccess(state, status) {
+      state.hasAccess = status
+    }
+  },
+  actions: {
+    // 登录相关动作
+    login({ commit }, { phoneNum, password }) {
+      return new Promise((resolve, reject) => {
+        login({ phoneNum, password }).then(
+          res => {
+            const { data } = res
+            setToken(data.token)
+            setAdmin(data.admin)
+            commit('setName', data.admin.name || data.user.userName)
+            resolve(res)
+          }
+        ).catch(error => {
+          reject(error)
+        })
+      })
+    },
+    // 退出登录相关动作
+    logout({ commit }) {
+      return new Promise((resolve, reject) => {
+        setToken('')
+        commit('setHasAccess', false)
+        resolve()
+      })
+    },
+    // 获取角色相关列表
+    userGetRole({ state, commit }) {
+      return new Promise((resolve, reject) => {
+        userGetRole().then(
+          res => {
+            const roleList = res.data
+            commit('setRoles', JSON.stringify(roleList))
+            resolve(roleList)
+          }
+        ).catch(error => {
+          reject(error)
+        })
+      })
+    },
+    // 检查用户相关信息
+    checkRole({ state, commit }) {
+      return new Promise((resolve, reject) => {
+        checkRole(state.token).then(
+          res => {
+            const access = res.data
+            commit('setAccess', access)
+            commit('setHasAccess', true)
+            resolve({ access })
+          }
+        ).catch(error => {
+          reject(error)
+        })
+      })
+    }
+  },
+  modules: {}
+}

+ 20 - 0
admin/src/utils/date.js

@@ -0,0 +1,20 @@
+export function fotmatDate (date, fmt='yyyy.MM.dd') {
+  if (!(date instanceof Date)) {
+    date = new Date(date)
+  }
+  var o = {   
+    "M+" : date.getMonth()+1,                 //月份   
+    "d+" : date.getDate(),                    //日   
+    "h+" : date.getHours(),                   //小时   
+    "m+" : date.getMinutes(),                 //分   
+    "s+" : date.getSeconds(),                 //秒   
+    "q+" : Math.floor((date.getMonth()+3)/3), //季度   
+    "S"  : date.getMilliseconds()             //毫秒   
+  };   
+  if(/(y+)/.test(fmt))   
+    fmt=fmt.replace(RegExp.$1, (date.getFullYear()+"").substr(4 - RegExp.$1.length));   
+  for(var k in o)   
+    if(new RegExp("("+ k +")").test(fmt))   
+  fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));   
+  return fmt;   
+}

+ 34 - 0
admin/src/utils/encode.js

@@ -0,0 +1,34 @@
+import { Base64 } from 'js-base64'
+
+function randomWord (randomFlag, min, max) {
+  let str = ''
+  let range = min
+  let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
+  // 随机产生
+  if (randomFlag) {
+    range = Math.round(Math.random() * (max - min)) + min
+  }
+  for (var i = 0; i < range; i++) {
+    let pos = Math.round(Math.random() * (arr.length - 1))
+    str += arr[pos]
+  }
+  return str
+}
+
+
+
+function encodeStr (str) {
+  const NUM = 2
+  const front = randomWord(false, 8)
+  const middle = randomWord(false, 8)
+  const end = randomWord(false, 8)
+
+  let str1 = str.substring(0, NUM)
+  let str2 = str.substring(NUM)
+
+  return front + str2 + middle + str1 + end
+}
+
+export default function (password) {
+  return encodeStr(Base64.encode(password))
+}

+ 0 - 0
admin/src/views/enterprise/index.vue


+ 21 - 0
admin/src/views/system/address/components/create.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="address-create">
+    <CreateForm :form="form" />
+  </div>
+</template>
+
+<script>
+import CreateForm from './form'
+export default {
+  props: {
+    form: Object
+  },
+  data () {
+    return {
+    }
+  },
+  components: {
+    CreateForm
+  }
+}
+</script>

+ 117 - 0
admin/src/views/system/address/components/form.vue

@@ -0,0 +1,117 @@
+<template>
+  <div>
+    <Form :label-width="75">
+      <FormItem label="区域类型">
+        <Select size="large" class="form-input" v-model="form.type" @on-change="fetchCityListByType">
+          <Option :value="item.value" :key="index" v-for="(item, index) in area_type">{{ item.label }}</Option>
+        </Select>
+        
+      </FormItem>
+      <FormItem label="上级区域">
+        <Select 
+          class="form-input" 
+          v-model="form.parent_name" 
+          filterable
+          remote
+          :remote-method="fetchCityList"
+          @on-change="change"
+          size="large"
+          :loading="searchLoading">
+          <Option :value="item.name" :key="item.id" v-for="item in parents">{{ item.name }}</Option>
+        </Select>
+      </FormItem>
+      <FormItem label="区域名称">
+        <Input size="large" class="form-input" v-model="form.name" />
+        
+      </FormItem>
+    </Form>
+    
+  </div>
+</template>
+
+<script>
+import { fetchCityList } from 'api/region'
+import pinyin from 'js-pinyin'
+export default {
+  props: {
+    form: Object,
+    isEdit: Boolean
+  },
+  data () {
+    return {
+      searchLoading: false,
+      area_type: [
+        {
+          value: 0,
+          label: '国家'
+        },
+        {
+          value: 1,
+          label: '省份'
+        },
+        {
+          value: 2,
+          label: '城市'
+        },
+        {
+          value: 3,
+          label: '区县'
+        },
+        {
+          value: 4,
+          label: '街道'
+        }
+      ],
+      parents: []
+    }
+  },
+  components: {
+  },
+  mounted () {
+    // this.fetchCityList('')
+  },
+  watch: {
+    'form.name' (val) {
+      this.form.first_alphabet = ''
+      if (val) {
+        this.form.first_alphabet = pinyin.getCamelChars(val).slice(0, 1)
+      }
+    }
+  },
+  methods: {
+    fetchCityListByType (type) {
+      this.form.parent_name = ''
+      this.parents = []
+      if (this.isEdit) {
+        this.fetchCityList('')
+        return
+      }
+      this.fetchCityList('', true)
+    },
+    fetchCityList (searchKey, noSetFirst) {
+      if (!isNaN(searchKey)) searchKey = ''
+      this.searchLoading = true
+      fetchCityList({type: this.form.type - 1, searchKey, page_size: 999, page_num: 1}).then(res => {
+        this.parents = res.data.list
+        if (!noSetFirst) {
+          this.form.parent_name = res.data.list[0] && res.data.list[0].name || ''
+        }
+        this.searchLoading = false
+      })
+    },
+    change (value) {
+      const findItem = this.parents.find(item => item.name === value)
+      console.log(findItem, value, this.parents)
+      if (findItem) {
+        this.form.parent_id = findItem.id
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.form-input {
+  width: 300px;
+}
+</style>

+ 122 - 0
admin/src/views/system/address/list.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="house-list">
+    <tables
+      :buttonList="buttonList"
+      :data-api="dataApi"
+      :delete-api="deleteApi"
+      deleteIdKey="id"
+      :columns="columns"
+      @create="create"
+      placeholder="上级区域/区域"
+      ref="table"
+      @toEdit="toEdit"
+    />
+    <CModal :show="show==='create'" title="新增地址" :width="420" @submit="createAddress" @close="show=''">
+      <createForm :form="form" v-if="show==='create'" />
+    </CModal>
+    <CModal :show="show==='edit'" title="编辑地址" :width="420" @submit="editAddress" @close="show=''">
+      <createForm :form="form" v-if="show==='edit'" isEdit />
+    </CModal>
+  </div>
+</template>
+
+<script>
+import tables from 'components/tables'
+import { fetchCityList, createCity, deleteRegion, updateRegion } from 'api/region'
+import createForm from './components/create'
+import CModal from 'components/Modal'
+const regionMap = {
+  0: '国家',
+  1: '省份',
+  2: '城市',
+  3: '区县',
+  4: '街道'
+}
+export default {
+  data() {
+    return {
+      show: '',
+      buttonList: [
+        {
+          text: '新增',
+          handle: 'create'
+        }
+      ],
+      dataApi: fetchCityList,
+      deleteApi: deleteRegion,
+      columns: [
+        {
+          type: 'index',
+          title: '序号',
+          align: 'center',
+          width: 80
+        },
+        {
+          title: '上级区域',
+          key: 'parent_name',
+          align: 'center'
+        },
+        {
+          title: '区域',
+          key: 'name',
+          align: 'center'
+        },
+        {
+          title: '类型',
+          key: 'type',
+          align: 'center',
+          render: (h, params) => {
+            return h('span', regionMap[params.row.type])
+          }
+        },
+        {
+          title: '操作',
+          slot: 'action',
+          tools: ['edit', 'del' ],
+          align: 'center'
+        }
+      ],
+      form: {
+        type: '',
+        parent_id: '0'
+      }
+    }
+  },
+  components: {
+    tables,
+    createForm,
+    CModal
+  },
+  methods: {
+    create () {
+      this.form = {
+        parent_id: '0'
+      }
+      this.show = 'create'
+    },
+    toEdit (row) {
+      this.show = 'edit'
+      this.form = JSON.parse(JSON.stringify(row))
+    },
+    async createAddress () {
+      console.log(this.form)
+      await createCity(this.form)
+      this.show = ''
+      this.$refs['table'].handleTableData()
+    },
+    async editAddress () {
+      await updateRegion(this.form)
+      this.show = ''
+      this.$refs['table'].handleTableData()
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.house-list {
+  padding: 20px;
+  background: #252828;
+}
+
+</style>

+ 9 - 0
admin/src/views/system/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <router-view />
+</template>
+
+<script>
+export default {
+  
+}
+</script>

+ 31 - 0
admin/src/views/system/store/components/create.vue

@@ -0,0 +1,31 @@
+<template>
+  <storeForm :showStatus="showStatus" :form="form" title="新增门店" @submit="submit" @close="close" />
+</template>
+
+<script>
+import storeForm from './form'
+import { createStore } from 'api/store'
+export default {
+  props: {
+    showStatus: Boolean,
+  },
+  data () {
+    return {
+      form: {}
+    }
+  },
+  components: {
+    storeForm
+  },
+  methods: {
+    submit () {
+      createStore(this.form).then(res => {
+        this.$emit('createStoreSuccess')
+      })
+    },
+    close () {
+      this.$emit('close')
+    }
+  }
+}
+</script>

+ 41 - 0
admin/src/views/system/store/components/edit.vue

@@ -0,0 +1,41 @@
+<template>
+  <storeForm :showStatus="showStatus" :form="form" title="修改门店" @submit="submit" @close="close" />
+</template>
+
+<script>
+import storeForm from './form'
+import { updateStore } from 'api/store'
+export default {
+  props: {
+    showStatus: Boolean,
+    form: {
+      type: Object,
+      default () {
+        return {
+          name: 1
+        }
+      }
+    }
+  },
+  data () {
+    return {
+    }
+  },
+  components: {
+    storeForm
+  },
+  watch: {
+
+  },
+  methods: {
+    submit () {
+      updateStore(this.form).then(res => {
+        this.$emit('editStoreSuccess')
+      })
+    },
+    close () {
+      this.$emit('close')
+    }
+  }
+}
+</script>

+ 60 - 0
admin/src/views/system/store/components/form.vue

@@ -0,0 +1,60 @@
+<template>
+  <Modal v-model="showStatus" width="350" footer-hide>
+    <div slot="close">
+      <Icon custom="iconfont iconform_close" @click="close" />
+    </div>
+    <div slot="header" class="modal-header">{{ title }}</div>
+    <template>
+      <Form :label-width="75">
+        <FormItem label="门店名称">
+          <Input size="large" v-model="form.name" class="form-input" maxlength="8" />
+        </FormItem>
+        <FormItem label="门店地址">
+          <Input size="large" v-model="form.address" class="form-input" />
+        </FormItem>
+        <FormItem label="门店状态">
+          <Select v-model="form.status" class="form-input" size="large">
+            <Option :value="1">启用</Option>
+            <Option :value="0">停用</Option>
+          </Select>
+        </FormItem>
+        <!-- <FormItem label="创建时间">
+          <Input  size="large" v-model="form.createTime" class="form-input" />
+        </FormItem> -->
+      </Form>
+      <div class="btn-w">
+        <Button type="primary" @click="submit">确定</Button>
+      </div>
+    </template>
+  </Modal>
+  
+</template>
+
+<script>
+export default {
+  props: {
+    form: Object,
+    title: String,
+    showStatus: Boolean
+  },
+  methods: {
+    submit () {
+      this.$emit('submit')
+    },
+    close () {
+      this.$emit('close')
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.modal-header {
+  line-height: 50px;
+  padding-left: 20px;
+  font-size: 16px;
+}
+.form-input {
+  width: 240px;
+}
+</style>

+ 129 - 0
admin/src/views/system/store/list.vue

@@ -0,0 +1,129 @@
+<template>
+  <div class="house-list">
+    <tables
+      :buttonList="buttonList"
+      :data-api="dataApi"
+      :delete-api="deleteApi"
+      :columns="columns"
+      @create="create"
+      @toEdit="editStore"
+      placeholder="门店名称"
+      deleteIdKey="id"
+      ref="table"
+    />
+    <createModal :showStatus="showStatus === 'create'" @close="showStatus=''" @createStoreSuccess="updateTable" />
+    <editModal :showStatus="showStatus === 'edit'" :form="editForm" @close="showStatus=''" @editStoreSuccess="updateTable" />
+  </div>
+</template>
+
+<script>
+import tables from 'components/tables'
+import { fetchStoreList, deleteStore, updateStore } from 'api/store'
+import createModal from './components/create'
+import editModal from './components/edit'
+const storeStatusMap = {
+  1: '启用',
+  2: '停用'
+}
+export default {
+  data() {
+    return {
+      // 传递给tables的表格列数据
+      editForm: {
+        status: '',
+        name: '12122'
+      },
+      buttonList: [
+        {
+          text: '新增',
+          handle: 'create'
+        }
+      ],
+      dataApi: fetchStoreList,
+      deleteApi: deleteStore,
+      columns: [
+        {
+          type: 'index',
+          title: '序号',
+          align: 'center',
+          width: 80
+        },
+        {
+          title: '门店id',
+          key: 'id',
+          align: 'center'
+        },
+        {
+          title: '门店名称',
+          key: 'name',
+          align: 'center'
+        },
+        {
+          title: '门店地址',
+          key: 'address',
+          align: 'center'
+        },
+        {
+          title: '状态',
+          key: 'status',
+          align: 'center',
+          render: (h, params) => {
+            return h('i-switch', {
+              props: {
+                trueValue: 1,
+                falseValue: 0,
+                value: params.row.status,
+                // disabled: params.row.roleKey === 'admin'
+              },
+              on: {
+                'on-change': (val) => {
+                  updateStore(Object.assign(params.row, {status: val}))
+                }
+              }
+            })
+          }
+        },
+        {
+          title: '创建时间',
+          key: 'createTime',
+          align: 'center'
+        },
+        {
+          title: '操作',
+          slot: 'action',
+          tools: ['edit', 'del'],
+          align: 'center'
+        }
+      ],
+      showStatus: ''
+    }
+  },
+  components: {
+    tables,
+    createModal,
+    editModal
+  },
+  methods: {
+    create () {
+      this.showStatus = 'create'
+    },
+    updateTable () {
+      this.$refs['table'].handleTableData()
+      this.showStatus = ''
+    },
+    editStore (item) {
+      this.showStatus = 'edit'
+      this.editForm = JSON.parse(JSON.stringify(item))
+      console.log(this.editForm)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.house-list {
+  padding: 20px;
+  background: #252828;
+}
+
+</style>

+ 151 - 0
admin/src/views/system/user/list.vue

@@ -0,0 +1,151 @@
+<template>
+  <div class="house-list">
+    <tables
+      :data-api="dataApi"
+      :columns="columns"
+      @toEdit="toEdit"
+      placeholder="账号名称"
+      ref="table"
+    />
+    <CModal :show="show" title="用户信息" :width="420" @submit="changeUser" @close="closeModal">
+      <Form :label-width="75">
+        <FormItem label="用户名称">
+          <Input size="large" v-model="form.name" disabled />
+        </FormItem>
+        <FormItem label="用户角色">
+          <Select v-model="form.type" size="large">
+            <Option :value="-1">未分配</Option>
+            <Option :value="item.roleType" v-for="item in roleList" :key="item.roleId">{{ item.remark }}</Option>
+          </Select>
+        </FormItem>
+        <FormItem label="手机号码">
+          <Input size="large" v-model="form.phone" disabled />
+        </FormItem>
+      </Form>
+    </CModal>
+  </div>
+</template>
+
+<script>
+import tables from 'components/tables'
+import { fetchUserList, updateUser, fetchRoleList } from 'api/'
+import CModal from 'components/Modal'
+
+const roleMap = {
+  0: '超级管理员',
+  1: '普通管理员',
+  2: '经纪人',
+}
+export default {
+  data() {
+    return {
+      show: false,
+      form: {},
+      // 传递给tables的表格列数据
+      dataApi: fetchUserList,
+      roleList: [],
+      columns: [
+        {
+          type: 'index',
+          title: '序号',
+          align: 'center',
+          width: 80
+        },
+        {
+          title: '账号ID',
+          key: 'admin_id',
+          align: 'center'
+        },
+        {
+          title: '账号名称',
+          key: 'name',
+          align: 'center'
+        },
+        {
+          title: '角色',
+          key: 'type',
+          align: 'center',
+          render: (h, params) => {
+            return h('span', roleMap[params.row.type])
+          }
+        },
+        {
+          title: '联系方式',
+          key: 'phone',
+          align: 'center'
+        },
+        {
+          title: '启动状态',
+          key: 'name',
+          align: 'center',
+          render: (h, params) => {
+            return h('i-switch', {
+              props: {
+                trueValue: 1,
+                falseValue: 0,
+                value: params.row.enable,
+                // disabled: params.row.roleKey === 'admin'
+              },
+              on: {
+                'on-change': (val) => {
+                  updateUser(Object.assign(params.row, {enable: val})).catch (err => {
+                    params.row.enable = !val ? 1 : 0
+                  })
+                }
+              }
+            })
+          }
+        },
+        {
+          title: '创建时间',
+          key: 'createTime',
+          align: 'center'
+        },
+        {
+          title: '操作',
+          slot: 'action',
+          tools: ['edit' ],
+          align: 'center'
+        }
+      ],
+    }
+  },
+  components: {
+    tables,
+    CModal
+  },
+  mounted () {
+    this.fetchRoleList()
+  },
+  methods: {
+    fetchRoleList () {
+      fetchRoleList().then(res => {
+        this.roleList = res.data
+      })
+    },
+    async changeUser () {
+      await updateUser(this.form)
+      this.closeModal()
+      this.$Message.success({
+        content: '修改成功'
+      })
+      this.$refs['table'].handleTableData()
+    },
+    toEdit (row) {
+      this.form = JSON.parse(JSON.stringify(row))
+      this.show = true
+    },
+    closeModal () {
+      this.show = false
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.house-list {
+  padding: 20px;
+  background: #252828;
+}
+
+</style>

+ 5 - 0
admin/tests/unit/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  env: {
+    jest: true
+  }
+}

+ 98 - 0
admin/tests/unit/components/Breadcrumb.spec.js

@@ -0,0 +1,98 @@
+import { mount, createLocalVue } from '@vue/test-utils'
+import VueRouter from 'vue-router'
+import ElementUI from 'element-ui'
+import Breadcrumb from '@/components/Breadcrumb/index.vue'
+
+const localVue = createLocalVue()
+localVue.use(VueRouter)
+localVue.use(ElementUI)
+
+const routes = [
+  {
+    path: '/',
+    name: 'home',
+    children: [{
+      path: 'dashboard',
+      name: 'dashboard'
+    }]
+  },
+  {
+    path: '/menu',
+    name: 'menu',
+    children: [{
+      path: 'menu1',
+      name: 'menu1',
+      meta: { title: 'menu1' },
+      children: [{
+        path: 'menu1-1',
+        name: 'menu1-1',
+        meta: { title: 'menu1-1' }
+      },
+      {
+        path: 'menu1-2',
+        name: 'menu1-2',
+        redirect: 'noredirect',
+        meta: { title: 'menu1-2' },
+        children: [{
+          path: 'menu1-2-1',
+          name: 'menu1-2-1',
+          meta: { title: 'menu1-2-1' }
+        },
+        {
+          path: 'menu1-2-2',
+          name: 'menu1-2-2'
+        }]
+      }]
+    }]
+  }]
+
+const router = new VueRouter({
+  routes
+})
+
+describe('Breadcrumb.vue', () => {
+  const wrapper = mount(Breadcrumb, {
+    localVue,
+    router
+  })
+  it('dashboard', () => {
+    router.push('/dashboard')
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
+    expect(len).toBe(1)
+  })
+  it('normal route', () => {
+    router.push('/menu/menu1')
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
+    expect(len).toBe(2)
+  })
+  it('nested route', () => {
+    router.push('/menu/menu1/menu1-2/menu1-2-1')
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
+    expect(len).toBe(4)
+  })
+  it('no meta.title', () => {
+    router.push('/menu/menu1/menu1-2/menu1-2-2')
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
+    expect(len).toBe(3)
+  })
+  // it('click link', () => {
+  //   router.push('/menu/menu1/menu1-2/menu1-2-2')
+  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
+  //   const second = breadcrumbArray.at(1)
+  //   console.log(breadcrumbArray)
+  //   const href = second.find('a').attributes().href
+  //   expect(href).toBe('#/menu/menu1')
+  // })
+  // it('noRedirect', () => {
+  //   router.push('/menu/menu1/menu1-2/menu1-2-1')
+  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
+  //   const redirectBreadcrumb = breadcrumbArray.at(2)
+  //   expect(redirectBreadcrumb.contains('a')).toBe(false)
+  // })
+  it('last breadcrumb', () => {
+    router.push('/menu/menu1/menu1-2/menu1-2-1')
+    const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
+    const redirectBreadcrumb = breadcrumbArray.at(3)
+    expect(redirectBreadcrumb.contains('a')).toBe(false)
+  })
+})

+ 18 - 0
admin/tests/unit/components/Hamburger.spec.js

@@ -0,0 +1,18 @@
+import { shallowMount } from '@vue/test-utils'
+import Hamburger from '@/components/Hamburger/index.vue'
+describe('Hamburger.vue', () => {
+  it('toggle click', () => {
+    const wrapper = shallowMount(Hamburger)
+    const mockFn = jest.fn()
+    wrapper.vm.$on('toggleClick', mockFn)
+    wrapper.find('.hamburger').trigger('click')
+    expect(mockFn).toBeCalled()
+  })
+  it('prop isActive', () => {
+    const wrapper = shallowMount(Hamburger)
+    wrapper.setProps({ isActive: true })
+    expect(wrapper.contains('.is-active')).toBe(true)
+    wrapper.setProps({ isActive: false })
+    expect(wrapper.contains('.is-active')).toBe(false)
+  })
+})

+ 22 - 0
admin/tests/unit/components/SvgIcon.spec.js

@@ -0,0 +1,22 @@
+import { shallowMount } from '@vue/test-utils'
+import SvgIcon from '@/components/SvgIcon/index.vue'
+describe('SvgIcon.vue', () => {
+  it('iconClass', () => {
+    const wrapper = shallowMount(SvgIcon, {
+      propsData: {
+        iconClass: 'test'
+      }
+    })
+    expect(wrapper.find('use').attributes().href).toBe('#icon-test')
+  })
+  it('className', () => {
+    const wrapper = shallowMount(SvgIcon, {
+      propsData: {
+        iconClass: 'test'
+      }
+    })
+    expect(wrapper.classes().length).toBe(1)
+    wrapper.setProps({ className: 'test' })
+    expect(wrapper.classes().includes('test')).toBe(true)
+  })
+})

+ 0 - 0
admin/tests/unit/utils/formatTime.spec.js


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.