张延森 6 vuotta sitten
vanhempi
commit
c670584af7
45 muutettua tiedostoa jossa 2011 lisäystä ja 0 poistoa
  1. 22
    0
      .gitignore
  2. 5
    0
      babel.config.js
  3. 58
    0
      package.json
  4. BIN
      public/favicon.ico
  5. 18
    0
      public/index.html
  6. 23
    0
      src/App.vue
  7. 21
    0
      src/assets/iconfont.css
  8. BIN
      src/assets/iconfont.eot
  9. 29
    0
      src/assets/iconfont.svg
  10. BIN
      src/assets/iconfont.ttf
  11. BIN
      src/assets/iconfont.woff
  12. BIN
      src/assets/logo.png
  13. 54
    0
      src/components/EditableInput.vue
  14. 69
    0
      src/components/EditableSelect.vue
  15. 46
    0
      src/components/XMFooter.vue
  16. 31
    0
      src/components/XMIcon.vue
  17. 137
    0
      src/components/XMPolygon.vue
  18. 21
    0
      src/components/XMRightLay.vue
  19. 30
    0
      src/components/XMSearchForm.vue
  20. 79
    0
      src/components/charts/StatCard.vue
  21. 88
    0
      src/components/charts/VisitingDayLine.vue
  22. 93
    0
      src/components/charts/VisitingDayTable.vue
  23. 91
    0
      src/components/imglist.vue
  24. 34
    0
      src/config/api.js
  25. 3
    0
      src/layout/RouterView.vue
  26. 43
    0
      src/layout/default/MenuItem.vue
  27. 25
    0
      src/layout/default/MenuLink.vue
  28. 52
    0
      src/layout/default/User.vue
  29. 136
    0
      src/layout/default/index.vue
  30. 20
    0
      src/layout/index.vue
  31. 26
    0
      src/main.js
  32. 55
    0
      src/router.js
  33. 14
    0
      src/store/index.js
  34. 19
    0
      src/store/modules/persons.js
  35. 59
    0
      src/store/system.js
  36. 5
    0
      src/theme.scss
  37. 6
    0
      src/utils/comptype.js
  38. 6
    0
      src/utils/httpcode.js
  39. 61
    0
      src/utils/request.js
  40. 199
    0
      src/views/Dashboard.vue
  41. 144
    0
      src/views/Login.vue
  42. 114
    0
      src/views/consultant/list.vue
  43. 46
    0
      src/views/index.js
  44. 3
    0
      src/views/index.vue
  45. 26
    0
      vue.config.js

+ 22
- 0
.gitignore Näytä tiedosto

@@ -0,0 +1,22 @@
1
+.DS_Store
2
+node_modules
3
+/dist
4
+package-lock.json
5
+
6
+# local env files
7
+.env.local
8
+.env.*.local
9
+
10
+# Log files
11
+npm-debug.log*
12
+yarn-debug.log*
13
+yarn-error.log*
14
+
15
+# Editor directories and files
16
+.idea
17
+.vscode
18
+*.suo
19
+*.ntvs*
20
+*.njsproj
21
+*.sln
22
+*.sw*

+ 5
- 0
babel.config.js Näytä tiedosto

@@ -0,0 +1,5 @@
1
+module.exports = {
2
+  presets: [
3
+    '@vue/app'
4
+  ]
5
+}

+ 58
- 0
package.json Näytä tiedosto

@@ -0,0 +1,58 @@
1
+{
2
+  "name": "estateagents",
3
+  "version": "0.1.0",
4
+  "private": true,
5
+  "scripts": {
6
+    "serve": "vue-cli-service serve",
7
+    "build": "vue-cli-service build",
8
+    "lint": "vue-cli-service lint"
9
+  },
10
+  "dependencies": {
11
+    "axios": "^0.18.0",
12
+    "blueimp-md5": "^2.10.0",
13
+    "dayjs": "^1.8.12",
14
+    "echarts": "^4.2.1",
15
+    "element-ui": "^2.6.1",
16
+    "normalize.css": "^8.0.1",
17
+    "nprogress": "^0.2.0",
18
+    "vue": "^2.6.6",
19
+    "vue-echarts": "^4.0.1",
20
+    "vue-router": "^3.0.2",
21
+    "vuex": "^3.1.0"
22
+  },
23
+  "devDependencies": {
24
+    "@vue/cli-plugin-babel": "^3.0.5",
25
+    "@vue/cli-plugin-eslint": "^3.0.5",
26
+    "@vue/cli-service": "^3.7.0",
27
+    "babel-eslint": "^10.0.1",
28
+    "eslint": "^5.8.0",
29
+    "eslint-plugin-vue": "^5.0.0",
30
+    "node-sass": "^4.11.0",
31
+    "sass-loader": "^7.1.0",
32
+    "vue-template-compiler": "^2.5.21"
33
+  },
34
+  "eslintConfig": {
35
+    "root": true,
36
+    "env": {
37
+      "node": true
38
+    },
39
+    "extends": [
40
+      "plugin:vue/essential",
41
+      "eslint:recommended"
42
+    ],
43
+    "rules": {},
44
+    "parserOptions": {
45
+      "parser": "babel-eslint"
46
+    }
47
+  },
48
+  "postcss": {
49
+    "plugins": {
50
+      "autoprefixer": {}
51
+    }
52
+  },
53
+  "browserslist": [
54
+    "> 1%",
55
+    "last 2 versions",
56
+    "not ie <= 8"
57
+  ]
58
+}

BIN
public/favicon.ico Näytä tiedosto


+ 18
- 0
public/index.html Näytä tiedosto

@@ -0,0 +1,18 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <meta charset="utf-8">
5
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
7
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
8
+    <link rel="stylesheet" href="//at.alicdn.com/t/font_1070150_8lyiyriedbr.css">
9
+    <title>迎宾系统</title>
10
+  </head>
11
+  <body>
12
+    <noscript>
13
+      <strong>We're sorry but welcome doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
14
+    </noscript>
15
+    <div id="app"></div>
16
+    <!-- built files will be auto injected -->
17
+  </body>
18
+</html>

+ 23
- 0
src/App.vue Näytä tiedosto

@@ -0,0 +1,23 @@
1
+<template>
2
+  <div id="app">
3
+    <router-view></router-view>
4
+  </div>
5
+</template>
6
+
7
+<script>
8
+
9
+export default {
10
+  name: 'app',
11
+  components: {
12
+  }
13
+}
14
+</script>
15
+
16
+<style>
17
+html, body, #app {
18
+  width: 100%;
19
+  height: 100%;
20
+
21
+  background-color: #f3f3f3;
22
+}
23
+</style>

+ 21
- 0
src/assets/iconfont.css Näytä tiedosto

@@ -0,0 +1,21 @@
1
+@font-face {font-family: "iconfont";
2
+  src: url('iconfont.eot?t=1552528558117'); /* IE9 */
3
+  src: url('iconfont.eot?t=1552528558117#iefix') format('embedded-opentype'), /* IE6-IE8 */
4
+  url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAALYAAsAAAAABoAAAAKMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCcAqBCIEZATYCJAMICwYABCAFhG0HMRvABciemjwJUmbD2AB8U3gADCEePvb7de5zxCUxNPEsmhgiXgKheaislrCUuCF5Cf9/1/TfcmNSy5vikhwgqGLuuOlL0zEpQOMm3ITaXEGNFcopzF7XhQxMPPWJe6f/N4ACme+8y2WMQWNNmtQFGAdSQHtjtIXLJPGGsQte4HkC9YZVEXY7+sYgScaeFoh7VXZDUsEnk7FkJRQNazOIJ8hVyX7cDfAYfT/+w1IkgUSegl12cNnug6ZfmU4aWvtfcx4Q4CegzTlSLAIZcdIY24MEYyD1MdE4OFZx8CvzvxZYxVFNgv11dmEjGITiZ5J40kflTUBGd2aAlUlvIm0qFhbD4bFIZDwWm4xGJ6Z9wlte3ulBd8F5wcS57+9d45MXf33oFEpeGonf3yJHEAj9sPjzjSDlT9YvxgFZGrMPpx2iXypdWa/fsXZ5qb1+XB5OFO+DTYDaB7pGTSAgOLgoS3dmrn4lJQL8vIoceWofIgxzgOoE9uBHSQY2ZYbMtiSZJmlMJWagG6ioV48a7Gp3P1XfdVepUOm6N0dpKEOqMklm9EXkGiyjUNlAvQWdmxt0YJDIkoF52wCh1RUSzX4g1eqWzOh3yHX7RKE1EOqdRceeDaZDTdlhxM2JD3vmseqlfsm18ZS4eITIIcPN8rKIOUFYUA1iu8VWTnYSP2FLLAjOyA7OJSwxauIOcBkxDIoDjGrEyy0K54Eaq1VqupHFS01IsYMh3DjCB/OYh6m8KD8paM+lVD4/gpCFGNxYR0OdfwLBBKm9Y3YWtgFkp9o/qOFRrgmaIXPgOAkmYSgT1gFGEQYDhQWaR2kIL85CmRAJqGF1WklD1Zbttebf7YB6tiEJZyiC8oqG5kJuPwA=') format('woff2'),
5
+  url('iconfont.woff?t=1552528558117') format('woff'),
6
+  url('iconfont.ttf?t=1552528558117') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
7
+  url('iconfont.svg?t=1552528558117#iconfont') format('svg'); /* iOS 4.1- */
8
+}
9
+
10
+.iconfont {
11
+  font-family: "iconfont" !important;
12
+  font-size: 16px;
13
+  font-style: normal;
14
+  -webkit-font-smoothing: antialiased;
15
+  -moz-osx-font-smoothing: grayscale;
16
+}
17
+
18
+.icon-gouxuan:before {
19
+  content: "\e60f";
20
+}
21
+

BIN
src/assets/iconfont.eot Näytä tiedosto


+ 29
- 0
src/assets/iconfont.svg Näytä tiedosto

@@ -0,0 +1,29 @@
1
+<?xml version="1.0" standalone="no"?>
2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
3
+<!--
4
+2013-9-30: Created.
5
+-->
6
+<svg>
7
+<metadata>
8
+Created by iconfont
9
+</metadata>
10
+<defs>
11
+
12
+<font id="iconfont" horiz-adv-x="1024" >
13
+  <font-face
14
+    font-family="iconfont"
15
+    font-weight="500"
16
+    font-stretch="normal"
17
+    units-per-em="1024"
18
+    ascent="896"
19
+    descent="-128"
20
+  />
21
+    <missing-glyph />
22
+    
23
+    <glyph glyph-name="gouxuan" unicode="&#58895;" d="M510.194376 831.24416c-246.595304 0-447.24416-200.649879-447.24416-447.245184 0-246.629073 200.614064-447.243137 447.24416-447.243137s447.24416 200.614064 447.24416 447.243137C957.438537 630.594281 756.82345 831.24416 510.194376 831.24416L510.194376 831.24416zM726.02091 457.77219 469.091236 198.048891c-0.064468-0.064468-0.191358-0.098237-0.260943-0.195451-0.092098-0.064468-0.092098-0.190335-0.190335-0.25378-2.051729-1.988284-4.587482-3.208065-6.962575-4.558829-1.183965-0.669242-2.148943-1.698688-3.40147-2.178619-3.848655-1.543146-7.92346-2.344395-12.002358-2.344395-4.107551 0-8.244778 0.801249-12.127202 2.408863-1.283226 0.543376-2.311649 1.635243-3.53143 2.310625-2.37407 1.346671-4.842285 2.536776-6.89913 4.552689-0.062422 0.064468-0.097214 0.195451-0.161682 0.25992-0.062422 0.094144-0.190335 0.094144-0.254803 0.192382L296.937364 328.102922c-12.353352 12.707416-12.06478 33.015951 0.641613 45.37135 12.70537 12.322653 32.985252 12.097526 45.373397-0.64366l103.546308-106.400309 233.92268 236.489132c12.45159 12.608156 32.79594 12.735046 45.37442 0.254803C738.33947 490.693997 738.467383 470.379323 726.02091 457.77219L726.02091 457.77219zM726.02091 457.77219"  horiz-adv-x="1024" />
24
+
25
+    
26
+
27
+
28
+  </font>
29
+</defs></svg>

BIN
src/assets/iconfont.ttf Näytä tiedosto


BIN
src/assets/iconfont.woff Näytä tiedosto


BIN
src/assets/logo.png Näytä tiedosto


+ 54
- 0
src/components/EditableInput.vue Näytä tiedosto

@@ -0,0 +1,54 @@
1
+<template>
2
+  <span @click="toggleFocus" v-if="!focus">
3
+    {{value}}
4
+    <span class="empty-tip" v-if="!value">(empty)</span>
5
+  </span>
6
+  <el-input ref="ctl" v-model="inputVal" @blur="handleBlur" v-else></el-input>
7
+</template>
8
+
9
+<script>
10
+export default {
11
+  name: 'editable-input',
12
+  props: [
13
+    'value',
14
+  ],
15
+  data () {
16
+    return {
17
+      focus: false,
18
+      inputVal: undefined,
19
+    }
20
+  },
21
+  mounted () {
22
+    if (!window['__editable-blur']) {
23
+      window['__editable-blur'] = () => {}
24
+    }
25
+  },
26
+  methods: {
27
+    toggleFocus () {
28
+      window['__editable-blur']()
29
+      window['__editable-blur'] = this.handleBlur.bind(this)
30
+
31
+      this.focus = true
32
+      this.inputVal = this.value
33
+      this.$nextTick(() => {
34
+        if (this.$refs.ctl) this.$refs.ctl.focus()
35
+      })
36
+    },
37
+    handleBlur() {
38
+      this.focus = false
39
+
40
+      if (this.inputVal != this.value) {
41
+        this.$emit('change', this.inputVal )
42
+      }
43
+    }
44
+  }
45
+}
46
+</script>
47
+
48
+<style lang="scss" scoped>
49
+.empty-tip {
50
+  font-size: 0.8em;
51
+  color: #aaa;
52
+}
53
+</style>
54
+

+ 69
- 0
src/components/EditableSelect.vue Näytä tiedosto

@@ -0,0 +1,69 @@
1
+<template>
2
+  <span v-if="!focus" @click.stop="toggleFocus">
3
+    {{valName}}
4
+    <span class="empty-tip" v-if="!value">(empty)</span>
5
+  </span>
6
+  <el-select ref="ctl" v-model="inputVal" @change="handleChange" v-else>
7
+    <el-option
8
+      v-for="item in dict"
9
+      :key="item.value"
10
+      :label="item.label"
11
+      :value="item.value">
12
+    </el-option>
13
+  </el-select>
14
+</template>
15
+
16
+<script>
17
+export default {
18
+  name: 'editable-select',
19
+  props: [
20
+    'value',
21
+    'dict',
22
+  ],
23
+  computed: {
24
+    valName () {
25
+      return ((this.dict || []).filter(x => x.value === this.value)[0] || {}).label
26
+    },
27
+  },
28
+  data () {
29
+    return {
30
+      focus: false,
31
+      inputVal: undefined,
32
+    }
33
+  },
34
+  mounted () {
35
+    if (!window['__editable-blur']) {
36
+      window['__editable-blur'] = () => {}
37
+    }
38
+  },
39
+  methods: {
40
+    toggleFocus () {
41
+      window['__editable-blur']()
42
+      window['__editable-blur'] = this.handleBlur.bind(this)
43
+
44
+      this.focus = true
45
+      this.inputVal = this.value
46
+      this.$nextTick(() => {
47
+        if (this.$refs.ctl) this.$refs.ctl.focus()
48
+      })
49
+    },
50
+    handleBlur () {
51
+      this.focus = false
52
+    },
53
+    handleChange() {
54
+      this.focus = false
55
+
56
+      if (this.inputVal != this.value) {
57
+        this.$emit('change', this.inputVal )
58
+      }
59
+    }
60
+  }
61
+}
62
+</script>
63
+
64
+<style lang="scss" scoped>
65
+.empty-tip {
66
+  font-size: 0.8em;
67
+  color: #aaa;
68
+}
69
+</style>

+ 46
- 0
src/components/XMFooter.vue Näytä tiedosto

@@ -0,0 +1,46 @@
1
+<template>
2
+  <div class="footer">
3
+    <div>{{copyright}}</div>
4
+    <div>{{company}}</div>
5
+  </div>
6
+</template>
7
+
8
+<script>
9
+export default {
10
+  name: 'xm-footer',
11
+  props: [
12
+    'company',
13
+  ],
14
+  data () {
15
+    return {}
16
+  },
17
+  computed: {
18
+    copyright() {
19
+      const year = (new Date()).getFullYear()
20
+      return `CopyRight @ ${year}`
21
+    }
22
+  }
23
+}
24
+</script>
25
+
26
+<style lang="scss">
27
+.footer {
28
+  display:flex;
29
+  justify-content:center;
30
+  align-items:center;
31
+  width: 100%;
32
+  height: 100%;
33
+  color: #666;
34
+  // text-align: center;
35
+  font-size: 14px;
36
+  font-weight: 500;
37
+  text-shadow: 1px 1px #fff;
38
+
39
+  div {
40
+    & + div {
41
+      padding-left: 12px;
42
+    }
43
+  }
44
+}
45
+</style>
46
+

+ 31
- 0
src/components/XMIcon.vue Näytä tiedosto

@@ -0,0 +1,31 @@
1
+<template>
2
+  <i
3
+    :class="{ iconfont: true, [`xm-${name}`]: true}"
4
+    :style="{ color: color || undefined, fontSize }"></i>
5
+</template>
6
+
7
+<script>
8
+export default {
9
+  name: 'xm-icon',
10
+  props: [
11
+    'name',
12
+    'color',
13
+    'size',
14
+  ],
15
+  computed: {
16
+    fontSize() {
17
+      switch (this.size) {
18
+        case 'large':
19
+          return '64px'
20
+        case 'medium':
21
+          return '48px'
22
+        case 'small':
23
+          return '24px'
24
+        case 'min':
25
+        default:
26
+          return '16px'
27
+      }
28
+    }
29
+  }
30
+}
31
+</script>

+ 137
- 0
src/components/XMPolygon.vue Näytä tiedosto

@@ -0,0 +1,137 @@
1
+<template>
2
+  <canvas></canvas>
3
+</template>
4
+
5
+<script>
6
+export default {
7
+  name: 'xmpolygon',
8
+  props: [
9
+    'color',
10
+    'dots',
11
+  ],
12
+  data() {
13
+    return {
14
+      interval: null,
15
+    }
16
+  },
17
+  mounted() {
18
+    if (this.$el && !this.interval) {
19
+      this.interval = canvasMotion(this.$el, this.color, this.dots) 
20
+    }
21
+  },
22
+  destroyed() {
23
+    if (this.interval) {
24
+      clearInterval(this.interval);
25
+      window.onmousemove = null;
26
+    }
27
+  }
28
+}
29
+
30
+function canvasMotion(canvas, color = '#044281', dotNum = 800) {
31
+  const ctx = canvas.getContext('2d');
32
+
33
+  canvas.width = window.innerWidth;
34
+  canvas.height = window.innerHeight;
35
+  canvas.style.display = 'block';
36
+
37
+  ctx.fillStyle = color;
38
+  ctx.lineWidth = .2;
39
+  ctx.strokeStyle = color;
40
+
41
+  const mousePosition = {
42
+      x: 30 * canvas.width / 100,
43
+      y: 30 * canvas.height / 100
44
+  };
45
+
46
+  const dots = {
47
+      nb: dotNum,
48
+      distance: 60,
49
+      d_radius: 100,
50
+      array: []
51
+  };
52
+
53
+  function Dot(){
54
+      this.x = Math.random() * canvas.width;
55
+      this.y = Math.random() * canvas.height;
56
+
57
+      this.vx = -.5 + Math.random();
58
+      this.vy = -.5 + Math.random();
59
+
60
+      this.radius = Math.random();
61
+  }
62
+
63
+  Dot.prototype = {
64
+      create: function(){
65
+          ctx.beginPath();
66
+          ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
67
+          ctx.fill();
68
+      },
69
+
70
+      animate: function(){
71
+          for(let i = 0; i < dots.nb; i++){
72
+
73
+              const dot = dots.array[i];
74
+
75
+              if(dot.y < 0 || dot.y > canvas.height){
76
+                  // dot.vx = dot.vx;
77
+                  dot.vy = - dot.vy;
78
+              }
79
+              else if(dot.x < 0 || dot.x > canvas.width){
80
+                  dot.vx = - dot.vx;
81
+                  // dot.vy = dot.vy;
82
+              }
83
+              dot.x += dot.vx;
84
+              dot.y += dot.vy;
85
+          }
86
+      },
87
+
88
+      line: function(){
89
+          for(let i = 0; i < dots.nb; i++){
90
+              for(let j = 0; j < dots.nb; j++){
91
+                  const i_dot = dots.array[i];
92
+                  const j_dot = dots.array[j];
93
+
94
+                  if((i_dot.x - j_dot.x) < dots.distance 
95
+                    && (i_dot.y - j_dot.y) < dots.distance 
96
+                    && (i_dot.x - j_dot.x) > - dots.distance 
97
+                    && (i_dot.y - j_dot.y) > - dots.distance) {
98
+                      if((i_dot.x - mousePosition.x) < dots.d_radius 
99
+                        && (i_dot.y - mousePosition.y) < dots.d_radius 
100
+                        && (i_dot.x - mousePosition.x) > - dots.d_radius 
101
+                        && (i_dot.y - mousePosition.y) > - dots.d_radius) {
102
+                          ctx.beginPath();
103
+                          ctx.moveTo(i_dot.x, i_dot.y);
104
+                          ctx.lineTo(j_dot.x, j_dot.y);
105
+                          ctx.stroke();
106
+                          ctx.closePath();
107
+                      }
108
+                  }
109
+              }
110
+          }
111
+      }
112
+  };
113
+
114
+  function createDots(){
115
+      ctx.clearRect(0, 0, canvas.width, canvas.height);
116
+
117
+      for(let i = 0; i < dots.nb; i++){
118
+          dots.array.push(new Dot());
119
+          dots.array[i].create();
120
+      }
121
+
122
+      const dot = dots.array[dots.nb - 1];
123
+      dot.line();
124
+      dot.animate();
125
+  }
126
+
127
+  window.onmousemove = function(e) {
128
+      mousePosition.x = e.pageX;
129
+      mousePosition.y = e.pageY;
130
+  }
131
+
132
+  mousePosition.x = window.innerWidth / 2;
133
+  mousePosition.y = window.innerHeight / 2;
134
+
135
+  return setInterval(createDots, 1000/30);
136
+}
137
+</script>

+ 21
- 0
src/components/XMRightLay.vue Näytä tiedosto

@@ -0,0 +1,21 @@
1
+<template>
2
+  <div class="right-lay">
3
+    <div></div>
4
+    <slot></slot>
5
+  </div>
6
+</template>
7
+
8
+<style lang="scss" scoped>
9
+  .right-lay {
10
+    display: flex;
11
+    justify-items: end;
12
+
13
+    & > div:first-child {
14
+      flex: auto;
15
+    }
16
+
17
+    & > * {
18
+      flex: none;
19
+    }
20
+  }
21
+</style>

+ 30
- 0
src/components/XMSearchForm.vue Näytä tiedosto

@@ -0,0 +1,30 @@
1
+<template>
2
+  <el-form :inline="true" size="small" class="search-form">
3
+    <slot></slot>
4
+    <el-form-item>
5
+      <el-button type="primary" @click="submit">搜索</el-button>
6
+    </el-form-item>
7
+    <slot name="tail"></slot>
8
+  </el-form>
9
+</template>
10
+
11
+<script>
12
+export default {
13
+  name: 'search-form',
14
+  methods: {
15
+    submit() {
16
+      this.$emit('submit')
17
+    },
18
+    resetForm() {
19
+      this.$refs.xform.resetFields()
20
+    }
21
+  },
22
+}
23
+</script>
24
+
25
+<style lang="scss" scoped>
26
+.search-form {
27
+
28
+}
29
+</style>
30
+

+ 79
- 0
src/components/charts/StatCard.vue Näytä tiedosto

@@ -0,0 +1,79 @@
1
+<template>
2
+  <el-card
3
+    shadow="hover"
4
+    :body-style="{ paddingLeft: 0, paddingRight: 0 }"
5
+    :class="{ 'card-box': true, [`theme-${theme || 'default'}`]: true }">
6
+    <el-row>
7
+      <el-col :span="10" class="icon">
8
+        <i :class="icon" v-if="icon"></i>
9
+        <span v-else>&nbsp;</span>
10
+      </el-col>
11
+      <el-col :span="14">
12
+        <div class="cat">{{tip}}</div>
13
+        <div class="val">{{val}}</div>
14
+      </el-col>
15
+    </el-row>
16
+  </el-card>
17
+</template>
18
+
19
+<script>
20
+export default {
21
+  name: 'statis-card',
22
+  props: [
23
+    'tip',
24
+    'val',
25
+    'icon',
26
+    'theme',
27
+  ]
28
+}
29
+</script>
30
+
31
+<style lang="scss" scoped>
32
+.card-box {
33
+  color: #333;
34
+  font-size: 16px;
35
+
36
+  .icon {
37
+    font-size: 2em;
38
+    text-align: left;
39
+    line-height: 1.4em;
40
+    text-align: center;
41
+  }
42
+
43
+  .cat {
44
+    font-size: 1em;
45
+    line-height: 1.2em;
46
+  }
47
+
48
+  .val {
49
+    font-size: 1.5em;
50
+    line-height: 1.2em;
51
+  }
52
+}
53
+
54
+.theme-default {
55
+  background-color: #fff;
56
+  color: #333;
57
+}
58
+
59
+.theme-primary {
60
+  background-color: #409eff;
61
+  color: #fff;
62
+}
63
+
64
+.theme-success {
65
+  background-color: #67c23a;
66
+  color: #fff;
67
+}
68
+
69
+.theme-warn {
70
+  background-color: #e6a23c;
71
+  color: #fff;
72
+}
73
+
74
+.theme-danger {
75
+  background-color: #f56c6c;
76
+  color: #fff;
77
+}
78
+</style>
79
+

+ 88
- 0
src/components/charts/VisitingDayLine.vue Näytä tiedosto

@@ -0,0 +1,88 @@
1
+<template>
2
+  <v-chart class="x-chart" :options="options" autoresize></v-chart>
3
+</template>
4
+
5
+<script>
6
+import 'echarts/lib/chart/line'
7
+import 'echarts/lib/component/title'
8
+import 'echarts/lib/component/dataZoom'
9
+import 'echarts/lib/component/tooltip'
10
+
11
+export default {
12
+  name: 'visiting-day-line',
13
+  components: {
14
+    // vChart: () => import('vue-echarts'),
15
+  },
16
+  props: [
17
+    'source',
18
+  ],
19
+  data () {
20
+    return {
21
+      opts: {
22
+        title: { text: '日访问量' },
23
+        tooltip: {
24
+          formatter: (params, ticket, callback) => {
25
+            const { visiteDate, amount } = params.data
26
+            const tip = `${visiteDate}: ${amount}`
27
+            callback(ticket, tip)
28
+            return tip
29
+          }
30
+        },
31
+        xAxis: { type: 'category' },
32
+        yAxis: { name: '人/日' },
33
+        series: [
34
+          {
35
+            type: 'line',
36
+            smooth: true,
37
+            encode: {
38
+              x: 'visiteDate',
39
+              y: 'amount',
40
+            },
41
+          },
42
+        ],
43
+        dataZoom: [
44
+          {
45
+            type: 'inside',
46
+            show: true,
47
+            start: 0,
48
+            end: 100,
49
+          },
50
+          // {
51
+          //   type: 'slider',
52
+          //   show: true,
53
+          //   start: 0,
54
+          //   end: 100,
55
+          // }
56
+        ],
57
+        dataset: {
58
+          sourceHeader: false,
59
+          dimensions: ['visiteDate', 'amount'],
60
+        },
61
+      }
62
+    }
63
+  },
64
+  computed: {
65
+    options () {
66
+      return {
67
+        ...this.opts,
68
+        title: {
69
+          ...this.opts.title,
70
+        },
71
+        dataset: {
72
+          ...this.opts.dataset,
73
+          source: this.source || [],
74
+        }
75
+      }
76
+    }
77
+  }
78
+}
79
+</script>
80
+
81
+<style lang="scss" scoped>
82
+.x-chart {
83
+  margin: 0 auto;
84
+  width: 100%;
85
+  height: 100%;
86
+}
87
+</style>
88
+

+ 93
- 0
src/components/charts/VisitingDayTable.vue Näytä tiedosto

@@ -0,0 +1,93 @@
1
+<template>
2
+  <div>
3
+    <h3 style="margin: 0">访客记录</h3>
4
+    <el-table
5
+      :data="dataSet"
6
+      stripe
7
+      style="width: 100%">
8
+      <el-table-column
9
+        label="日期"
10
+        width="180">
11
+        <template slot-scope="scope">
12
+          <span>{{formatDate(scope.row.visiteDate)}}</span>
13
+        </template>
14
+      </el-table-column>
15
+      <el-table-column
16
+        prop="name"
17
+        label="姓名"
18
+        width="180">
19
+      </el-table-column>
20
+      <el-table-column
21
+        prop="carStyle"
22
+        label="车辆">
23
+      </el-table-column>
24
+      <el-table-column
25
+        prop="deviceName"
26
+        label="采集设备">
27
+      </el-table-column>
28
+    </el-table>
29
+    <el-pagination
30
+      small
31
+      style="margin-top:10px;"
32
+      layout="prev, pager, next"
33
+      :page-size="pageNavi.size"
34
+      :total="pageNavi.total"
35
+      :current-page="pageNavi.current"
36
+      @current-change="changePage"
37
+    >
38
+    </el-pagination>
39
+  </div>
40
+</template>
41
+
42
+<script>
43
+import dayjs from 'dayjs'
44
+
45
+export default {
46
+  name: 'visiting-day-table',
47
+  props: [
48
+    'source',
49
+    'page',
50
+  ],
51
+  data() {
52
+    return {
53
+      sampleData: [
54
+        { visiteDate: '2019-02-15 17:00', name: '张一', carStyle: '奔驰', channel: '搜房' },
55
+        { visiteDate: '2019-02-15 16:40', name: '张二', carStyle: '奔驰', channel: '搜房' },
56
+        { visiteDate: '2019-02-15 16:30', name: '张三', carStyle: '奔驰', channel: '搜房' },
57
+        { visiteDate: '2019-02-15 15:50', name: '张四', carStyle: '奔驰', channel: '搜房' },
58
+        { visiteDate: '2019-02-15 15:40', name: '张五', carStyle: '奔驰', channel: '搜房' },
59
+        { visiteDate: '2019-02-15 15:20', name: '张六', carStyle: '奔驰', channel: '搜房' },
60
+        { visiteDate: '2019-02-15 15:10', name: '张七', carStyle: '奔驰', channel: '搜房' },
61
+        { visiteDate: '2019-02-15 15:00', name: '张八', carStyle: '奔驰', channel: '搜房' },
62
+      ],
63
+      defPage: {
64
+        current: 1,
65
+        size: 10,
66
+        total: 0,
67
+      }
68
+    }
69
+  },
70
+  computed: {
71
+    pageNavi () {
72
+      return {
73
+        ...this.defPage,
74
+        ...(this.page || {})
75
+      }
76
+    },
77
+    dataSet () {
78
+      return this.source || this.sampleData
79
+    }
80
+  },
81
+  methods: {
82
+    changePage (nwPg) {
83
+      this.$emit('page-change', nwPg)
84
+    },
85
+    formatDate(dt) {
86
+      if (!dt || dt.indexOf('0001-') > -1) return ''
87
+
88
+      return dayjs(dt).format('YYYY-MM-DD HH:mm')
89
+    }
90
+  }
91
+}
92
+</script>
93
+

+ 91
- 0
src/components/imglist.vue Näytä tiedosto

@@ -0,0 +1,91 @@
1
+<template>
2
+  <div class="listBox">
3
+    <a v-for="(item, index) in imgList" :key="index" @click="selectImg(item)">
4
+      <img :src="item.url" class="contain" alt>
5
+    </a>
6
+  </div>
7
+</template>
8
+
9
+<script>
10
+export default {
11
+  name: 'imglist',
12
+  props: [
13
+    'imgList'
14
+  ],
15
+  methods: {
16
+    selectImg (item) {
17
+      this.imgList.filter()
18
+    }
19
+  }
20
+}
21
+</script>
22
+
23
+<style lang="scss" scoped>
24
+.listBox {
25
+  font-size: 0;
26
+}
27
+.listBox > * {
28
+  display: inline-block;
29
+  margin: 15px 15px 0 0;
30
+  position: relative;
31
+  overflow: hidden;
32
+}
33
+.listBox > *:first-child {
34
+}
35
+.listBox > a {
36
+  width: 148px;
37
+  height: 148px;
38
+  box-sizing: border-box;
39
+  border: 1px dashed #c0ccda;
40
+  border-radius: 6px;
41
+  background: #fff;
42
+}
43
+
44
+.listBox > a:hover {
45
+  cursor: pointer;
46
+}
47
+
48
+.listBox > a::after {
49
+  content: "";
50
+  width: 100%;
51
+  position: absolute;
52
+  left: 0;
53
+  top: 0;
54
+  bottom: 0;
55
+  background: rgba(0, 0, 0, 0.5);
56
+  z-index: 10;
57
+  display: none;
58
+}
59
+
60
+.listBox > a::before {
61
+  content: "删除";
62
+  position: absolute;
63
+  left: 50%;
64
+  top: 50%;
65
+  transform: translate(-50%, -50%);
66
+  -webkit-transform: translate(-50%, -50%);
67
+  text-align: center;
68
+  color: #fff;
69
+  font-size: 16px;
70
+  z-index: 11;
71
+  display: none;
72
+}
73
+
74
+.listBox > a:hover::before,
75
+.listBox > a:hover::after {
76
+  display: block;
77
+}
78
+
79
+.listBox > a img {
80
+  display: block;
81
+  width: 100%;
82
+  height: 100%;
83
+  object-fit: contain;
84
+  position: absolute;
85
+  left: 50%;
86
+  top: 50%;
87
+  transform: translate(-50%, -50%);
88
+  -webkit-transform: translate(-50%, -50%);
89
+  z-index: 1;
90
+}
91
+</style>

+ 34
- 0
src/config/api.js Näytä tiedosto

@@ -0,0 +1,34 @@
1
+const commPrefix = '/api/admin'
2
+
3
+const apis = {
4
+  system: {
5
+    signin: {
6
+      url: `${commPrefix}/signin`,
7
+      method: 'post',
8
+    },
9
+    signoff: {
10
+      url: `${commPrefix}/signoff`,
11
+      method: 'post',
12
+    },
13
+    currentUser: {
14
+      url: `${commPrefix}/user/current`,
15
+      method: 'get',
16
+    },
17
+  },
18
+  consultant: {
19
+    list: {
20
+      url: `${commPrefix}/consultant`,
21
+      method: 'get',
22
+    },
23
+    add: {
24
+      url: `${commPrefix}/consultant`,
25
+      method: 'post',
26
+    },
27
+    edit: {
28
+      url: `${commPrefix}/consultant/:id`,
29
+      method: 'put',
30
+    },
31
+  },
32
+}
33
+
34
+export default apis

+ 3
- 0
src/layout/RouterView.vue Näytä tiedosto

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

+ 43
- 0
src/layout/default/MenuItem.vue Näytä tiedosto

@@ -0,0 +1,43 @@
1
+<template>
2
+  <el-submenu v-if="checkChildren(menu) && isShow(menu)" :index="menu.name">
3
+    <template slot="title">
4
+      <xm-icon v-if="menu.meta.icon" :name="menu.meta.icon"></xm-icon>
5
+      <span slot="title">{{ menu.meta.title }}</span>
6
+    </template>
7
+    <menu-item v-for="item in menu.children" :key="item.name" :menu="item" :validate="validate" />
8
+  </el-submenu>
9
+  <menu-link v-else-if="isShow(menu)" :to="menu.name">
10
+    <el-menu-item :index="menu.name">
11
+      <xm-icon v-if="menu.meta.icon" :name="menu.meta.icon"></xm-icon>
12
+      <span slot="title">{{ menu.meta.title }}</span>
13
+    </el-menu-item>
14
+  </menu-link>
15
+</template>
16
+
17
+<script>
18
+
19
+export default {
20
+  name: 'menu-item',
21
+  components: {
22
+    menuLink: () => import('./MenuLink.vue'),
23
+  },
24
+  props: [
25
+    'menu',
26
+    'validate',
27
+  ],
28
+  methods: {
29
+    checkChildren(menu) {
30
+      if (!menu.children || !menu.children.length) return false
31
+      
32
+      return menu.children.some(x => x.meta.menuShow)
33
+    },
34
+    isShow(item) {
35
+      if (!item.meta.menuShow) return false
36
+      if (!this.validate(item)) return false
37
+
38
+      return true
39
+    }
40
+  }
41
+}
42
+</script>
43
+

+ 25
- 0
src/layout/default/MenuLink.vue Näytä tiedosto

@@ -0,0 +1,25 @@
1
+<template>
2
+  <a class="noStyle" :href="to" v-if="isExternal(to)" target="_blank"><slot></slot></a>
3
+  <router-link class="noStyle" :to="{ name: to }" v-else><slot></slot></router-link>
4
+</template>
5
+
6
+<script>
7
+export default {
8
+  name: 'menu-link',
9
+  props: [
10
+    'to',
11
+  ],
12
+  methods: {
13
+    isExternal(path) {
14
+      return /^(https?:|mailto:|tel:)/.test(path)
15
+    }
16
+  }
17
+}
18
+</script>
19
+
20
+<style lang="scss" scoped>
21
+.noStyle {
22
+  text-decoration: none;
23
+}
24
+</style>
25
+

+ 52
- 0
src/layout/default/User.vue Näytä tiedosto

@@ -0,0 +1,52 @@
1
+<template>
2
+  <div class="userboard">
3
+    <!-- <el-badge :is-dot="!!(messages && messages.length)" class="message">
4
+      <xm-icon name="ring" :style="{ fontSize: '18px' }"></xm-icon>
5
+    </el-badge> -->
6
+    <el-dropdown @command="clickUser">
7
+      <span>
8
+        <xm-icon name="user" :style="{ fontSize: '18px' }"></xm-icon>
9
+        <xm-icon name="arrowdown"></xm-icon>
10
+      </span>
11
+      <el-dropdown-menu slot="dropdown">
12
+        <!-- <el-dropdown-item command="resetPassword">重置密码</el-dropdown-item> -->
13
+        <el-dropdown-item command="logout">登出</el-dropdown-item>
14
+      </el-dropdown-menu>
15
+    </el-dropdown>
16
+  </div>
17
+</template>
18
+
19
+<script>
20
+export default {
21
+  name: 'user-board',
22
+  props: [
23
+    'messages',
24
+    'user',
25
+  ],
26
+  methods: {
27
+    clickUser(cmd) {
28
+      switch (cmd) {
29
+        case 'resetPassword':
30
+          this.$emit('resetPassword')
31
+          break
32
+        case 'logout':
33
+          this.$emit('logout')
34
+          break
35
+        default:
36
+          // todo
37
+      }
38
+    }
39
+  }
40
+}
41
+</script>
42
+
43
+
44
+<style lang="scss" scoped>
45
+.userboard {
46
+  .message {
47
+    margin-right: 12px;
48
+    vertical-align: baseline;
49
+  }
50
+}
51
+</style>
52
+

+ 136
- 0
src/layout/default/index.vue Näytä tiedosto

@@ -0,0 +1,136 @@
1
+<template>
2
+  <el-container>
3
+    <el-aside class="siderbar" :style="{ width: '210px' }">
4
+      <div class="logo"></div>
5
+      <el-menu class="nav-menu" v-bind="menuParams">
6
+        <menu-item v-for="menu in menus" :key="menu.name" :menu="menu" :validate="isInMenuList" />
7
+      </el-menu>
8
+    </el-aside>
9
+    <el-container>
10
+      <el-header class="head">
11
+        <div class="h-item">&nbsp;</div>
12
+        <user class="h-item-noflex" :messages="[1]" @logout="logout" @resetPassword="resetPassword"></user>
13
+      </el-header>
14
+      <el-breadcrumb class="breadcrumb" separator-class="el-icon-arrow-right">
15
+        <el-breadcrumb-item :to="{ name: 'index' }">首页</el-breadcrumb-item>
16
+        <el-breadcrumb-item v-for="bread in breads" :key="bread.name">{{ (bread.meta || {}).title }}</el-breadcrumb-item>
17
+      </el-breadcrumb>
18
+      <el-main class="main">
19
+        <router-view></router-view>
20
+      </el-main>
21
+      <el-footer class="noPadding">
22
+        <xm-footer company="荟房科技" />
23
+      </el-footer>
24
+    </el-container>
25
+  </el-container>
26
+</template>
27
+
28
+<script>
29
+import { pages, pageArray } from '@/views'
30
+
31
+export default {
32
+  name: 'default-layout',
33
+  components: {
34
+    xmFooter: () => import('@/components/XMFooter.vue'),
35
+    menuItem: () => import('./MenuItem.vue'),
36
+    user: () => import('./User.vue'),
37
+  },
38
+  data () {
39
+    return {
40
+      menus: pages,
41
+      menuArray: pageArray,
42
+    }
43
+  },
44
+  computed: {
45
+    menuParams() {
46
+      return {
47
+        backgroundColor: '#24292e',
48
+        textColor: '#eee',
49
+        activeTextColor: '#FFD04B',
50
+        defaultActive: this.$route.name,
51
+      }
52
+    },
53
+    breads() {
54
+      const route = this.menuArray.filter(x => x.name === this.$route.name)[0] || {}
55
+      const p = (route.parents || []).map((rn) => {
56
+        return this.menuArray.filter(x => x.name === rn.name)[0];
57
+      })
58
+
59
+      return p.concat(route)
60
+    }
61
+  },
62
+  methods: {
63
+    isInMenuList(menu) {
64
+      return !!this.menuArray.filter(x => x.name === menu.name)[0]
65
+    },
66
+    resetPassword() {
67
+      window.console.log('------resetPassword------')
68
+    },
69
+    logout() {
70
+      this.$store.dispatch('logout').then(() => {
71
+        this.$router.push({
72
+          name: 'login',
73
+          query: {
74
+            from: this.$route.name,
75
+          }
76
+        })
77
+      })
78
+    }
79
+  }
80
+}
81
+</script>
82
+
83
+<style lang="scss" scoped>
84
+.noPadding {
85
+  padding: 0;
86
+}
87
+
88
+.head {
89
+  display: flex;
90
+  align-items: center;
91
+  justify-items: end;
92
+  background-color: #fafafa;
93
+  border-bottom: 1px solid #dfdfdf;
94
+
95
+  .h-item {
96
+    flex: auto;
97
+  }
98
+
99
+  .h-item-noflex {
100
+    flex: none;
101
+  }
102
+}
103
+
104
+.breadcrumb {
105
+  margin: 12px 20px;
106
+}
107
+
108
+.logo {
109
+  
110
+}
111
+
112
+.siderbar {
113
+  border-right: solid 1px #e6e6e6;
114
+  
115
+  color: #eee;
116
+  background-color: #24292e;
117
+
118
+  .nav-menu {
119
+    border: none;
120
+  }
121
+}
122
+
123
+.main {
124
+  /*160 = header + footer + margin */
125
+  min-height: calc(100vh - 160px);
126
+  position: relative;
127
+  overflow: hidden;
128
+  margin: 0 20px;
129
+  padding: 0;
130
+
131
+  & > * {
132
+    background-color: #fff;
133
+    padding: 20px;
134
+  }
135
+}
136
+</style>

+ 20
- 0
src/layout/index.vue Näytä tiedosto

@@ -0,0 +1,20 @@
1
+<template>
2
+  <component :is="currentComponent"></component>
3
+</template>
4
+
5
+<script>
6
+export default {
7
+  name: 'xm-layout',
8
+  props: [
9
+    'theme', // 有可能来自 store
10
+  ],
11
+  components: {
12
+    defaultLayout: () => import('./default'),
13
+  },
14
+  computed: {
15
+    currentComponent() {
16
+      return `${this.theme || 'default'}-layout`
17
+    }
18
+  }
19
+}
20
+</script>

+ 26
- 0
src/main.js Näytä tiedosto

@@ -0,0 +1,26 @@
1
+import Vue from 'vue'
2
+import Element from 'element-ui'
3
+import ECharts from 'vue-echarts'
4
+import XMIcon from '@/components/XMIcon.vue'
5
+import XMRightLay from '@/components/XMRightLay.vue'
6
+import XMSearchForm from '@/components/XMSearchForm.vue'
7
+import App from './App.vue'
8
+import router from './router'
9
+import store from './store'
10
+import 'normalize.css/normalize.css'
11
+import './theme.scss'
12
+import './assets/iconfont.css'
13
+
14
+Vue.use(Element)
15
+
16
+Vue.config.productionTip = false
17
+Vue.component('v-chart', ECharts)
18
+Vue.component('xm-icon', XMIcon)
19
+Vue.component('xm-rtl', XMRightLay)
20
+Vue.component('xm-search', XMSearchForm)
21
+
22
+new Vue({
23
+  render: h => h(App),
24
+  router,
25
+  store,
26
+}).$mount('#app')

+ 55
- 0
src/router.js Näytä tiedosto

@@ -0,0 +1,55 @@
1
+import Vue from 'vue'
2
+import VueRouter from 'vue-router'
3
+import NProgress from 'nprogress' // progress bar
4
+import 'nprogress/nprogress.css'
5
+import store from './store'
6
+import { pages } from './views'
7
+
8
+Vue.use(VueRouter)
9
+
10
+const routes = [
11
+  {
12
+    path: '/',
13
+    name: 'index',
14
+    redirect: '/dashboard',
15
+    component: () => import('@/layout/index.vue'),
16
+    children: [
17
+      ...pages,
18
+    ]
19
+  },
20
+  {
21
+    path: '/login',
22
+    name: 'login',
23
+    component: () => import('@/views/Login.vue')
24
+  }
25
+]
26
+
27
+const router = new VueRouter({ routes })
28
+
29
+// 校验用户权限
30
+router.beforeEach((to, from, next) => {
31
+  NProgress.start()
32
+  if (to.name === 'login') {
33
+    next()
34
+    return
35
+  }
36
+  store.dispatch('checkLogin').then(() => {
37
+    if (to.name === 'login') {
38
+      next({ name: 'index' })
39
+    } else {
40
+      next()
41
+    }
42
+  }).catch(() => {
43
+    if (to.name === 'login') {
44
+      next()
45
+    } else {
46
+      next({ name: 'login', query: { from: to.name } })
47
+    }
48
+  })
49
+})
50
+
51
+router.afterEach(() => {
52
+  NProgress.done()
53
+})
54
+
55
+export default router

+ 14
- 0
src/store/index.js Näytä tiedosto

@@ -0,0 +1,14 @@
1
+import Vue from 'vue'
2
+import Vuex from 'vuex'
3
+import system from './system'
4
+
5
+Vue.use(Vuex)
6
+
7
+const store = new Vuex.Store({
8
+  ...system,
9
+  modules: {
10
+    persons: require('./modules/persons').default,
11
+  }
12
+})
13
+
14
+export default store

+ 19
- 0
src/store/modules/persons.js Näytä tiedosto

@@ -0,0 +1,19 @@
1
+import apis from '../../config/api'
2
+import request from '../../utils/request'
3
+
4
+export default {
5
+  namespaced: true,
6
+  state: {
7
+    list: [],
8
+    detail: {},
9
+  },
10
+  mutations: {},
11
+  actions: {
12
+    getConsultants (context, payload) {
13
+      return request({
14
+        ...apis.consultant.list,
15
+        params: payload,
16
+      })
17
+    }
18
+  },
19
+}

+ 59
- 0
src/store/system.js Näytä tiedosto

@@ -0,0 +1,59 @@
1
+import request from '../utils/request'
2
+import apis from '../config/api'
3
+
4
+export default {
5
+  state: {
6
+    app: {
7
+      logo: '',
8
+      name: '荟房科技',
9
+      version: 1.0,
10
+    },
11
+    menus: [],
12
+    user: {},
13
+  },
14
+  mutations: {
15
+    updateUser(state, user) {
16
+      state.user = {
17
+        ...state.user,
18
+        ...user,
19
+      }
20
+    }
21
+  },
22
+  actions: {
23
+    checkLogin({ state, commit }) {
24
+      return new Promise((resolve, reject) => {
25
+        if (state.user.username) {
26
+          resolve()
27
+          return
28
+        }
29
+
30
+        request(apis.system.currentUser).then((data) => {
31
+          commit('updateUser', data)
32
+          resolve()
33
+        }).catch(() => {
34
+          reject()
35
+        })
36
+      })
37
+    },
38
+    login({ commit }, payload) {
39
+      return new Promise((resolve, reject) => {
40
+        request({
41
+          ...apis.system.signin,
42
+          data: payload,
43
+        }).then((data) => {
44
+          localStorage.setItem('x-token', data.token)
45
+          commit('updateUser', data)
46
+          resolve()
47
+        }).catch((err) => {
48
+          reject(err)
49
+        })
50
+      })
51
+    },
52
+    logout({ commit }) {
53
+      request(apis.system.signoff).then(() => {
54
+        localStorage.removeItem('x-token')
55
+        commit('updateUser', {})
56
+      })
57
+    }
58
+  }
59
+}

+ 5
- 0
src/theme.scss Näytä tiedosto

@@ -0,0 +1,5 @@
1
+$--color-primary: #044281;
2
+$--color-success: #35c155;
3
+
4
+$--font-path: '~element-ui/lib/theme-chalk/fonts';
5
+@import "~element-ui/packages/theme-chalk/src/index";

+ 6
- 0
src/utils/comptype.js Näytä tiedosto

@@ -0,0 +1,6 @@
1
+export const CTYP_GROUP = 'G-';
2
+export const CTYP_SINGLE_LINE = 'single-line';
3
+export const CTYP_MULTI_LINE = 'multi-line';
4
+export const CTYP_IMAGE = 'image';
5
+export const CTYP_MAP = 'map';
6
+export const CTYP_CONTACT_FORM = `${CTYP_GROUP}contact_form`;

+ 6
- 0
src/utils/httpcode.js Näytä tiedosto

@@ -0,0 +1,6 @@
1
+export default {
2
+  HTTP_OK: 200,
3
+  HTTP_MULTIPLECHOICES: 300,
4
+  HTTP_BADREQUEST: 400,
5
+  HTTP_UNAUTHORIZED: 401
6
+}

+ 61
- 0
src/utils/request.js Näytä tiedosto

@@ -0,0 +1,61 @@
1
+import axios from 'axios'
2
+
3
+// 远程请求, 返回 promise
4
+// 参数 api 与 axios 基本一致
5
+// 增加了一个 urlData 字段, 用来设置 url path 变量值
6
+function request(params = {}) {
7
+  const { urlData, data: rawData, url: rawURL, headers = {}, ...config } = params;
8
+  const url = replaceApiParams(rawURL, urlData)
9
+  const data = (rawData instanceof FormData) ? rawData : JSON.stringify(rawData)
10
+  const token = localStorage.getItem('x-token') || ''
11
+  const contenType = (rawData instanceof FormData) ? 'multipart/form-data' : 'application/json'
12
+  
13
+  return new Promise((resolve, reject) => {
14
+    axios({
15
+      ...config,
16
+      url,
17
+      data,
18
+      headers: {
19
+        'Content-Type': contenType,
20
+        ...headers,
21
+        'Authorization': `Bearer ${token}`,
22
+      }
23
+    }).then(({ data: res }) => {
24
+
25
+      // 服务端返回包含 code, msg, data 字段
26
+      const { code, data } = res
27
+
28
+      // 业务处理正确则返回 1000
29
+      if (code === 1000) {
30
+        resolve(data)
31
+      } else {
32
+        reject(res)
33
+      }
34
+
35
+    }).catch((error) => {
36
+      // 异常情况
37
+      if (error.response) {
38
+        reject({ msg: error.response.statusText })
39
+      } else {
40
+        reject({ msg: error.message })
41
+      }
42
+    })
43
+  })
44
+}
45
+
46
+function replaceApiParams (url, params) {
47
+  if (typeof params !== 'object') return url
48
+
49
+  return url.split('/').map((it) => {
50
+    if (it.indexOf(':') === 0) {
51
+      const theKey = it.substr(1)
52
+      return Object.prototype.hasOwnProperty.call(params, theKey)
53
+        ? params[theKey]
54
+        : it
55
+    }
56
+
57
+    return it
58
+  }).join('/')
59
+}
60
+
61
+export default request

+ 199
- 0
src/views/Dashboard.vue Näytä tiedosto

@@ -0,0 +1,199 @@
1
+<template>
2
+  <div class="dash-main">
3
+    <el-row :gutter="24" class="sta-row">
4
+      <el-col :span="8"><stat-card icon="el-icon-time" theme="success" tip="总到访" :val="personDt.total"></stat-card></el-col>
5
+      <el-col :span="8"><stat-card icon="el-icon-date" tip="今日到访" theme="danger" :val="personDt.today"></stat-card></el-col>
6
+      <el-col :span="8"><stat-card icon="el-icon-news" tip="今日老客户" theme="warn" :val="personDt.regular"></stat-card></el-col>
7
+    </el-row>
8
+    <div class="sta-row">
9
+      <el-card shadow="never">
10
+        <div slot="header" class="short-filters">
11
+          <div class="flex-item">&nbsp;</div>
12
+          <div class="static-item item-space">
13
+            <el-radio-group
14
+              size="small"
15
+              v-model="filterData.dateShortcut"
16
+              @change="handeShortDateChange"
17
+            >
18
+              <el-radio-button label="本年"></el-radio-button>
19
+              <el-radio-button label="本季"></el-radio-button>
20
+              <el-radio-button label="本月"></el-radio-button>
21
+            </el-radio-group>
22
+          </div>
23
+          <div class="static-item item-space">
24
+            <el-date-picker
25
+              size="small"
26
+              v-model="filterData.dateRange"
27
+              type="daterange"
28
+              range-separator="至"
29
+              start-placeholder="开始日期"
30
+              end-placeholder="结束日期"
31
+              @change="handleDateChange">
32
+            </el-date-picker>
33
+          </div>
34
+        </div>
35
+        <el-row :gutter="12" type="flex">
36
+          <el-col :span="12">
37
+            <table-chart :page="visitingPage" :source="visitingList" @page-change="nextVistingLogPage"></table-chart>
38
+          </el-col>
39
+          <el-col :span="12">
40
+            <line-chart :source="dailyVisiting" :style="{minHeight: '400px'}"></line-chart>
41
+          </el-col>
42
+        </el-row>
43
+      </el-card>
44
+    </div>
45
+  </div>
46
+</template>
47
+
48
+<script>
49
+import dayjs from 'dayjs'
50
+import { createNamespacedHelpers } from 'vuex'
51
+
52
+const {mapState: mapStatsState, mapActions: mapStatsActions} = createNamespacedHelpers('stats')
53
+
54
+export default {
55
+  name: 'dashboard',
56
+  components: {
57
+    lineChart: () => import('@/components/charts/VisitingDayLine.vue'),
58
+    tableChart: () => import('@/components/charts/VisitingDayTable.vue'),
59
+    statCard: () => import('@/components/charts/StatCard.vue'),
60
+  },
61
+  data () {
62
+    return {
63
+      filterData: {
64
+        dateRange: [],
65
+        dateShortcut: '',
66
+      },
67
+      visitingLogCurrentPage: 1,
68
+    }
69
+  },
70
+  computed: {
71
+    ...mapStatsState({
72
+      personDts: x => x.person,
73
+      dailyVisiting: x => x.visitingDaily,
74
+      visitingLog: x => x.visitingLog,
75
+    }),
76
+    personDt () {
77
+      return (this.personDts || [])[0] || {}
78
+    },
79
+    visitingPage () {
80
+      return {
81
+        total: (this.visitingLog || {}).total || 0,
82
+        size: (this.visitingLog || {}).size || 10,
83
+        current: this.visitingLogCurrentPage,
84
+      }
85
+    },
86
+    visitingList () {
87
+      return (this.visitingLog || {}).records || []
88
+    }
89
+  },
90
+  created () {
91
+    this.changeDate('week')
92
+    this.getPersonStats()
93
+  },
94
+  methods: {
95
+    ...mapStatsActions([
96
+      'getPersonStats',
97
+      'getVisitingDaily',
98
+      'getVisitingLog',
99
+    ]),
100
+    handleDateChange (dts) {
101
+      this.filterData.dateRange = dts
102
+      this.filterData.dateShortcut = ''
103
+      this.search()
104
+    },
105
+    search (rg) {
106
+      if (!rg || rg === 'visiting-daily') {
107
+        this.getVisitingDaily({
108
+          startDate: dayjs(this.filterData.dateRange[0]).format('YYYY-MM-DDT00:00:00'),
109
+          endDate: dayjs(this.filterData.dateRange[1]).format('YYYY-MM-DDT23:59:59'),
110
+        })
111
+      }
112
+
113
+      if (!rg || rg === 'visiting-log') {
114
+        this.getVisitingLog({
115
+          pageNum: this.visitingLogCurrentPage,
116
+          pageSize: this.visitingPage.size,
117
+          beginDate: dayjs(this.filterData.dateRange[0]).format('YYYY-MM-DDT00:00:00'),
118
+          endDate: dayjs(this.filterData.dateRange[1]).format('YYYY-MM-DDT23:59:59'),
119
+        })
120
+      }
121
+    },
122
+    changeDate (typ) {
123
+      this.filterData.dateRange = this.getDateRange(typ)
124
+      this.search()
125
+    },
126
+    nextVistingLogPage (page) {
127
+      this.visitingLogCurrentPage = page
128
+      this.search('visiting-log')
129
+    },
130
+    handeShortDateChange (val) {
131
+      switch (val) {
132
+        case '本年':
133
+          this.changeDate('year')
134
+          break
135
+        case '本季':
136
+          this.changeDate('quarter')
137
+          break
138
+        case '本月':
139
+          this.changeDate('month')
140
+          break
141
+      }
142
+    },
143
+    getDateRange(typ) {
144
+      // typ: https://github.com/iamkun/dayjs/blob/dev/src/constant.js#L13
145
+      const now = new Date();
146
+
147
+      const start = typ === 'quarter' ? this.getQuarterStart(now) : dayjs(now).startOf(typ).toDate()
148
+
149
+      return [ start, now ]
150
+    },
151
+    getQuarterStart(dt) {
152
+      const mon = dt.getMonth()
153
+      const year = dt.getFullYear()
154
+
155
+      if (mon < 3) return dayjs(`${year}-01-01`).toDate()
156
+      if (mon < 6) return dayjs(`${year}-03-01`).toDate()
157
+      if (mon < 9) return dayjs(`${year}-06-01`).toDate()
158
+
159
+      return dayjs(`${year}-09-01`).toDate()
160
+    }
161
+  }
162
+}
163
+</script>
164
+
165
+
166
+<style lang="scss" scoped>
167
+.dash-main {
168
+  background-color: transparent !important;
169
+  padding: 20px 0;
170
+}
171
+
172
+.sta-row {
173
+  & + .sta-row {
174
+    margin-top: 24px;
175
+  }
176
+}
177
+
178
+.short-filters {
179
+  display: flex;
180
+
181
+  .item-space {
182
+    margin-left: 24px;
183
+  }
184
+
185
+  .flex-item {
186
+    flex: auto;
187
+  }
188
+
189
+  .static-item {
190
+    flex: none;
191
+  }
192
+}
193
+</style>
194
+
195
+<style lang="scss">
196
+.dash-main .el-card__header {
197
+  padding: 12px;
198
+}
199
+</style>

+ 144
- 0
src/views/Login.vue Näytä tiedosto

@@ -0,0 +1,144 @@
1
+<template>
2
+  <div class="login-form" @keyup.enter="submit">
3
+    <xm-polygon color="#044281" :dots="800" class="xm-polygon" />
4
+    <el-container class="login-layout">
5
+      <el-main>
6
+        <div class="head">
7
+          <div class="logo"></div>
8
+          <div class="title">
9
+            <h2>欢迎使用荟房科技后台系统</h2>
10
+          </div>
11
+        </div>
12
+        <div class="body">
13
+          <el-form size="medium" :model="formdata">
14
+            <el-form-item>
15
+              <el-input class="large-input" v-model="formdata.username" placeholder="请输入手机号">
16
+                <xm-icon name="user" size="small" slot="prefix"/>
17
+              </el-input>
18
+            </el-form-item>
19
+            <el-form-item>
20
+              <el-input class="large-input" v-model="formdata.password" placeholder="请输入验证码">
21
+                <xm-icon name="password" size="small" slot="prefix" :style="{ lineHeight: '36px' }"/>
22
+                <el-button slot="append">发送验证码</el-button>
23
+              </el-input>
24
+            </el-form-item>
25
+            <el-form-item>
26
+              <div class="form-action">
27
+                <el-button type="primary" @click="submit">登录</el-button>
28
+              </div>
29
+            </el-form-item>
30
+          </el-form>
31
+        </div>
32
+      </el-main>
33
+      <el-footer class="foot">
34
+        <xm-footer company="荟房科技" />
35
+      </el-footer>
36
+    </el-container>
37
+  </div>
38
+</template>
39
+
40
+<script>
41
+import { mapActions } from 'vuex'
42
+// import md5 from 'blueimp-md5'
43
+
44
+export default {
45
+  name: 'login',
46
+  components: {
47
+    xmFooter: () => import('@/components/XMFooter.vue'),
48
+    xmPolygon: () => import('@/components/XMPolygon.vue'),
49
+  },
50
+  data() {
51
+    return {
52
+      formdata: {
53
+        username: '',
54
+        password: '',
55
+      },
56
+    }
57
+  },
58
+  computed: {
59
+    fromRoute() {
60
+      return this.$route.query.from || 'index'
61
+    }
62
+  },
63
+  methods: {
64
+    ...mapActions([
65
+      'login',
66
+    ]),
67
+    submit() {
68
+      this.login({ phone: this.formdata.username, captcha: this.formdata.password }).then(() => {
69
+        this.$router.push({ name: this.fromRoute })
70
+      }).catch((err) => {
71
+        this.$message({
72
+          showClose: true,
73
+          message: err.msg || err.message,
74
+          type: 'error'
75
+        });
76
+      })
77
+    }
78
+  }
79
+}
80
+</script>
81
+
82
+<style lang="scss" scoped>
83
+.login-form {
84
+  width: 100%;
85
+  height: 100%;
86
+
87
+  .login-layout {
88
+    padding: 0;
89
+    height: 100%;
90
+    z-index: 100;
91
+    position: relative;
92
+
93
+    .head {
94
+      margin: 10em auto 4em;
95
+      text-align: center;
96
+    }
97
+
98
+    .body {
99
+      width: 400px;
100
+      margin: 0 auto;
101
+
102
+      .form-action {
103
+        margin: 24px 0;
104
+
105
+        button {
106
+          width: 100%;
107
+          font-size: 16px;
108
+          padding: 0.64em 20px;
109
+        }
110
+      }
111
+    }
112
+
113
+    .foot {
114
+      padding: 0;
115
+    }
116
+  }
117
+
118
+  .xm-polygon {
119
+    position: absolute;
120
+    width: 100%;
121
+    height: 100%;
122
+    z-index: 0;
123
+
124
+    top: 0;
125
+    bottom: 0;
126
+    left: 0;
127
+    right: 0;
128
+  }
129
+}
130
+</style>
131
+
132
+<style lang="scss">
133
+.login-form {
134
+  .large-input {
135
+    font-size: 18px;
136
+
137
+    .el-input__inner {
138
+      height: 2em;
139
+      line-height: 2.2em;
140
+    }
141
+  }
142
+}
143
+</style>
144
+

+ 114
- 0
src/views/consultant/list.vue Näytä tiedosto

@@ -0,0 +1,114 @@
1
+<template>
2
+<div class="list">
3
+  <el-table
4
+    :data="list || []"
5
+    style="width: 100%">
6
+    <el-table-column
7
+      prop="personId"
8
+      label="#">
9
+    </el-table-column>
10
+    <el-table-column
11
+      prop="name"
12
+      label="名称">
13
+    </el-table-column>
14
+    <el-table-column
15
+      prop="tel"
16
+      label="电话">
17
+    </el-table-column>
18
+    <el-table-column
19
+      prop="hotNum"
20
+      label="人气">
21
+    </el-table-column>
22
+    <el-table-column
23
+      prop="likeNum"
24
+      label="点赞">
25
+    </el-table-column>
26
+    <el-table-column
27
+      fixed="right"
28
+      label="操作">
29
+      <template slot-scope="scope">
30
+        <el-button type="text" @click="toDetail(scope.row)" size="small">详情</el-button>
31
+      </template>
32
+    </el-table-column>
33
+  </el-table>
34
+  <el-pagination
35
+    small
36
+    style="margin-top:10px;"
37
+    layout="prev, pager, next"
38
+    :current-page.sync="pageNavi.current"
39
+    :pageSize="pageNavi.size"
40
+    :total="pageNavi.total || 0"
41
+    @current-change="getConsultants"
42
+  >
43
+  </el-pagination>
44
+</div>
45
+</template>
46
+
47
+<script>
48
+import { createNamespacedHelpers } from 'vuex'
49
+
50
+const { mapActions: mapPersonActions } = createNamespacedHelpers('persons')
51
+
52
+export default {
53
+  name: 'consultant-list',
54
+  data () {
55
+    return {
56
+      filterData: {
57
+        name: '',
58
+        phone: '',
59
+      },
60
+      list: [],
61
+      pageNavi: {
62
+        current: 1,
63
+        size: 1,
64
+        total: 0,
65
+      },
66
+    }
67
+  },
68
+  computed: {
69
+  },
70
+  methods: {
71
+    ...mapPersonActions([
72
+      'getConsultants',
73
+    ]),
74
+    getList () {
75
+      const pageNumber = this.pageNavi.current || 1
76
+      const pageSize = this.pageNavi.size
77
+
78
+      this.getConsultants({
79
+        ...this.filterData,
80
+        pageNumber,
81
+        pageSize,
82
+      }).then((res) => {
83
+        const { records, ...pageNavi } = res
84
+
85
+        this.list = records
86
+        this.pageNavi = pageNavi
87
+      }).catch((err) => {
88
+        this.$notify.error(err.msg || err.message)
89
+      })
90
+    },
91
+    toDetail (row) {
92
+      this.$router.push({ name: 'consultant.edit', params: { id: row.personId } })
93
+    },
94
+    newPage (page) {
95
+      this.$router.replace({ name: 'consultant.list', query: { page } })
96
+    }
97
+  },
98
+  created () {
99
+    this.pageNavi.current = this.$route.query.page || 1
100
+
101
+    this.getList()
102
+  }
103
+}
104
+</script>
105
+
106
+<style lang="scss" scoped>
107
+  .list{
108
+    .header{
109
+      width: 50px;
110
+      height: 50px;
111
+      border-radius: 50%;
112
+    }
113
+  }
114
+</style>

+ 46
- 0
src/views/index.js Näytä tiedosto

@@ -0,0 +1,46 @@
1
+
2
+const pages = [  
3
+  {
4
+    path: 'dashboard',
5
+    name: 'dashboard',
6
+    component: () => import('./Dashboard.vue'),
7
+    meta: {
8
+      menuShow: true,
9
+      title: 'Dashboard',
10
+    }
11
+  },
12
+  {
13
+    path: 'consultant',
14
+    name: 'consultant',
15
+    redirect: 'consultant/list',
16
+    component: () => import('./index.vue'),
17
+    meta: {
18
+      menuShow: true,
19
+      title: '置业管理',
20
+    },
21
+    children: [
22
+      {
23
+        path: 'list',
24
+        name: 'consultant.list',
25
+        component: () => import('./consultant/list.vue'),
26
+        meta: {
27
+          menuShow: true,
28
+          title: '置业列表',
29
+        },
30
+      },
31
+    ]
32
+  }
33
+]
34
+
35
+const flatten = (rts, parents = []) => {
36
+  return rts.reduce((acc, rt) => {
37
+    const chs = rt.children && rt.children.length ?
38
+      flatten(rt.children, parents.concat(rt)) : []
39
+    
40
+    return [...acc, { ...rt, parents }, ...chs]
41
+  }, [])
42
+}
43
+
44
+const pageArray = flatten(pages)
45
+
46
+export { pages, pageArray }

+ 3
- 0
src/views/index.vue Näytä tiedosto

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

+ 26
- 0
vue.config.js Näytä tiedosto

@@ -0,0 +1,26 @@
1
+module.exports = {
2
+  publicPath: './',
3
+  devServer: {
4
+    proxy: {
5
+      '/api': {
6
+        target: 'http://192.168.0.11:8080',
7
+        changeOrigin: true,
8
+        // pathRewrite: {
9
+        //   '^/api': '/'
10
+        // },
11
+      },
12
+      // '/api/websocket': {
13
+      //   target: 'ws://localhost:8080',
14
+      //   changeOrigin: true,
15
+      //   ws: true,
16
+      //   pathRewrite: {
17
+      //     '^/api': '/'
18
+      //   },
19
+      // },
20
+    }
21
+  },
22
+  transpileDependencies: [
23
+    'vue-echarts',
24
+    'resize-detector'
25
+  ]
26
+}