zjxpcyc 6 år sedan
förälder
incheckning
f42c92a716

+ 2
- 0
package.json Visa fil

@@ -13,10 +13,12 @@
13 13
     "axios": "^0.18.0",
14 14
     "blueimp-md5": "^2.10.0",
15 15
     "dayjs": "^1.8.12",
16
+    "echarts": "^4.2.1",
16 17
     "element-ui": "^2.6.1",
17 18
     "normalize.css": "^8.0.1",
18 19
     "nprogress": "^0.2.0",
19 20
     "vue": "^2.6.6",
21
+    "vue-echarts": "^4.0.1",
20 22
     "vue-router": "^3.0.2",
21 23
     "vuex": "^3.1.0"
22 24
   },

+ 54
- 0
src/components/EditableInput.vue Visa fil

@@ -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 Visa fil

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

+ 79
- 0
src/components/charts/StatCard.vue Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

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

+ 26
- 2
src/config/api.js Visa fil

@@ -55,6 +55,18 @@ const apis = {
55 55
       url: `${commPrefix}/person_type/:id`
56 56
     }
57 57
   },
58
+  stats: {
59
+    person: {
60
+      method: 'get',
61
+      url: `${commPrefix}/stats/person`
62
+    },
63
+    daily: {
64
+      visiting: {
65
+        method: 'get',
66
+        url: `${commPrefix}/stats/daily/visitinglog`
67
+      }
68
+    }
69
+  },
58 70
   notice: {
59 71
     add: {
60 72
       method: 'post',
@@ -101,7 +113,11 @@ const apis = {
101 113
     dispatch: {
102 114
       method: 'put',
103 115
       url: `${commPrefix}/person/:id/avatar`
104
-    }
116
+    },
117
+    merge: {
118
+      method: 'put',
119
+      url: `${commPrefix}/merge/person/:id`
120
+    },
105 121
   },
106 122
   guard: {
107 123
     dispatch: {
@@ -173,7 +189,15 @@ const apis = {
173 189
     list: {
174 190
       method: 'get',
175 191
       url: `${commPrefix}/device`
176
-    }
192
+    },
193
+    snapshot: {
194
+      method: 'get',
195
+      url: `${commPrefix}/snapshot`
196
+    },
197
+    dispatch: {
198
+      method: 'post',
199
+      url: `${commPrefix}/device/person`
200
+    },
177 201
   },
178 202
   words: {
179 203
     add: {

+ 6
- 1
src/layout/default/index.vue Visa fil

@@ -125,7 +125,12 @@ export default {
125 125
   min-height: calc(100vh - 160px);
126 126
   position: relative;
127 127
   overflow: hidden;
128
-  background-color: #fff;
129 128
   margin: 0 20px;
129
+  padding: 0;
130
+
131
+  & > * {
132
+    background-color: #fff;
133
+    padding: 20px;
134
+  }
130 135
 }
131 136
 </style>

+ 2
- 0
src/main.js Visa fil

@@ -1,5 +1,6 @@
1 1
 import Vue from 'vue'
2 2
 import Element from 'element-ui'
3
+import ECharts from 'vue-echarts'
3 4
 import XMIcon from '@/components/XMIcon.vue'
4 5
 import XMRightLay from '@/components/XMRightLay.vue'
5 6
 import XMSearchForm from '@/components/XMSearchForm.vue'
@@ -13,6 +14,7 @@ import './assets/iconfont.css'
13 14
 Vue.use(Element)
14 15
 
15 16
 Vue.config.productionTip = false
17
+Vue.component('v-chart', ECharts)
16 18
 Vue.component('xm-icon', XMIcon)
17 19
 Vue.component('xm-rtl', XMRightLay)
18 20
 Vue.component('xm-search', XMSearchForm)

+ 1
- 0
src/router.js Visa fil

@@ -11,6 +11,7 @@ const routes = [
11 11
   {
12 12
     path: '/',
13 13
     name: 'index',
14
+    redirect: '/dashboard',
14 15
     component: () => import('@/layout/index.vue'),
15 16
     children: [
16 17
       ...pages,

+ 2
- 0
src/store/index.js Visa fil

@@ -10,6 +10,7 @@ import words from './modules/words'
10 10
 import person from './modules/person'
11 11
 import sysparam from './modules/sysparam'
12 12
 import sysuser from './modules/sysuser'
13
+import stats from './modules/stats'
13 14
 
14 15
 Vue.use(Vuex)
15 16
 
@@ -25,6 +26,7 @@ const store = new Vuex.Store({
25 26
     person,
26 27
     sysparam,
27 28
     sysuser,
29
+    stats,
28 30
   }
29 31
 })
30 32
 

+ 26
- 2
src/store/modules/device.js Visa fil

@@ -5,12 +5,19 @@ import apis from '../../config/api'
5 5
 export default {
6 6
   namespaced: true,
7 7
   state: {
8
-    devices: {}
8
+    devices: {},
9
+    snapshots: {},
9 10
   },
10 11
   mutations: {
11 12
     updateList (state, payload) {
12 13
       state.devices = payload
13
-    }
14
+    },
15
+    updateState (state, payload = {}) {
16
+      Object.keys(payload).forEach((key) => {
17
+        const val = payload[key]
18
+        state[key] = Array.isArray(val) ? [ ...val ] : { ...val }
19
+      })
20
+    },
14 21
   },
15 22
   actions: {
16 23
     getDeviceList ({ commit }, payload) {
@@ -25,6 +32,23 @@ export default {
25 32
           }
26 33
         })
27 34
       })
35
+    },
36
+    getSnapshots ({ commit }, payload) {
37
+      return new Promise((resolve, reject) => {
38
+        const api = lodash.get(apis, 'device.snapshot')
39
+        interact(api, payload).then((data) => {
40
+          commit('updateState', { snapshots: data })
41
+          resolve(data)
42
+        }).catch(({ message }) => {
43
+          if (typeof message === 'string') {
44
+            reject(message)
45
+          }
46
+        })
47
+      })
48
+    },
49
+    dispatchBatch (ctx, payload) {
50
+      const api = lodash.get(apis, 'device.dispatch')
51
+      return interact({ ...api, params: payload })
28 52
     }
29 53
   }
30 54
 }

+ 23
- 1
src/store/modules/person.js Visa fil

@@ -15,13 +15,23 @@ export default {
15 15
       const { records } = payload
16 16
       state.persons = records
17 17
     },
18
+    mergeList (state, payload) {
19
+      state.persons = state.persons.map((person) => {
20
+        return person.personId === payload.personId ? { ...payload } : person
21
+      })
22
+    },
18 23
     updateGuards (state, payload) {
19 24
       const { records } = payload
20 25
       state.guards = records
21 26
     },
22 27
     updateDetail (state, payload) {
23 28
       state.detail = payload.detail || {}
24
-      state.visitingLog = payload.visitingLog || []
29
+      // state.visitingLog = payload.visitingLog || []
30
+    },
31
+    updateLog (state, payload) {
32
+      state.visitingLog = (state.visitingLog || []).map((log) => {
33
+        return log.logId === payload.logId ? { ...payload } : log
34
+      })
25 35
     },
26 36
     resetVisitingLog (state, payload) {
27 37
       state.visitingLog = payload || []
@@ -74,6 +84,18 @@ export default {
74 84
         commit('updateDetail', data)
75 85
       })
76 86
     },
87
+    getPurPerson (ctx, payload) {
88
+      const api = replaceApiParams(lodash.get(apis, 'person.detail'), payload)
89
+      return interact(api);
90
+    },
91
+    mergePerson ({ commit }, payload) {
92
+      const { from, to } = payload
93
+      const api = replaceApiParams(lodash.get(apis, 'person.merge'), { id: from })
94
+      return interact(api, JSON.stringify(to)).then(() => {
95
+        const person = { ...to, personId: from }
96
+        commit('mergeList', person)
97
+      })
98
+    },
77 99
     addPerson ( _, payload) {
78 100
       const { onSuccess } = payload
79 101
       const api = lodash.get(apis, 'person.add')

+ 63
- 0
src/store/modules/stats.js Visa fil

@@ -0,0 +1,63 @@
1
+import lodash from 'lodash'
2
+import { interact } from '../../utils'
3
+import apis from '../../config/api'
4
+
5
+export default {
6
+  namespaced: true,
7
+  state: {
8
+    person: [],
9
+    visitingDaily: [],
10
+    visitingLog: [],
11
+  },
12
+  mutations: {
13
+    updateStat (state, payload) {
14
+      Object.keys(payload).forEach((k) => {
15
+        const val = payload[k]
16
+        state[k] = Array.isArray(val) ? [ ...val ] : { ...val }
17
+      })
18
+    },
19
+  },
20
+  actions: {
21
+    getPersonStats ({ commit }, payload) {
22
+      return new Promise((resolve, reject) => {
23
+        const api = lodash.get(apis, 'stats.person')
24
+        interact(api, payload).then((data) => {
25
+          commit('updateStat', { person :data })
26
+          resolve(data)
27
+        }).catch(({ message }) => {
28
+          if (typeof message === 'string') {
29
+            reject(message)
30
+          }
31
+        })
32
+      })
33
+    },
34
+    
35
+    getVisitingDaily ({ commit }, payload) {
36
+      return new Promise((resolve, reject) => {
37
+        const api = lodash.get(apis, 'stats.daily.visiting')
38
+        interact(api, payload).then((data) => {
39
+          commit('updateStat', { visitingDaily :data })
40
+          resolve(data)
41
+        }).catch(({ message }) => {
42
+          if (typeof message === 'string') {
43
+            reject(message)
44
+          }
45
+        })
46
+      })
47
+    },
48
+    
49
+    getVisitingLog ({ commit }, payload) {
50
+      return new Promise((resolve, reject) => {
51
+        const api = lodash.get(apis, 'visitinglog.list')
52
+        interact(api, payload).then((data) => {
53
+          commit('updateStat', { visitingLog :data })
54
+          resolve(data)
55
+        }).catch(({ message }) => {
56
+          if (typeof message === 'string') {
57
+            reject(message)
58
+          }
59
+        })
60
+      })
61
+    },
62
+  }
63
+}

+ 191
- 6
src/views/Dashboard.vue Visa fil

@@ -1,14 +1,199 @@
1 1
 <template>
2
-  <div>
3
-    <h1>欢迎使用荟房迎宾系统</h1>
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>
4 45
   </div>
5 46
 </template>
6 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
+
7 166
 <style lang="scss" scoped>
8
-h1 {
9
-  text-align: center;
10
-  color: #333;
11
-  margin-top: 200px;
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
+  }
12 192
 }
13 193
 </style>
14 194
 
195
+<style lang="scss">
196
+.dash-main .el-card__header {
197
+  padding: 12px;
198
+}
199
+</style>

+ 186
- 3
src/views/device/list.vue Visa fil

@@ -41,6 +41,14 @@
41 41
         <span>{{FormatDate(scope.row.onlineDate)}}</span>
42 42
       </template>
43 43
     </el-table-column>
44
+    <el-table-column
45
+      fixed="right"
46
+      label="操作">
47
+      <template slot-scope="scope">
48
+        <el-button type="text" @click="startDispatch(scope.row)" size="small">下发</el-button>
49
+        <el-button type="text" @click="gotoSnapshotList(scope.row)"  size="small">抓拍流水</el-button>
50
+      </template>
51
+    </el-table-column>
44 52
   </el-table>
45 53
   <el-pagination
46 54
     small
@@ -52,6 +60,31 @@
52 60
     @current-change="getDevices"
53 61
   >
54 62
   </el-pagination>
63
+  <el-dialog title="人脸批量下发" :visible.sync="dipatchProcess.showDialog" width="30%">
64
+    <el-alert
65
+      :title="`下发需要${ !duration ? '一段时间' : '大约 ' + duration + ' 分钟' }, 过程中, 请不要进行其他操作, 请耐心等候下发完成!`"
66
+      type="warning"
67
+      :closable="false">
68
+    </el-alert>
69
+
70
+    <el-row :style="{ marginTop: '20px' }">
71
+      <el-col :span="18">
72
+        <div :style="{ margin: '0 auto', width: '130px' }">
73
+          <el-progress type="circle" :percentage="dealInfo.percentage" :status="dealInfo.status">{{ `${dealInfo.percentage}%` }}</el-progress>
74
+        </div>
75
+      </el-col>
76
+      <el-col :span="6">
77
+        <el-button
78
+          type="primary"
79
+          @click="dispatchToDevice"
80
+          :loading="dipatchProcess.step == 'process'"
81
+          :disabled="dipatchProcess.step == 'finish'"
82
+          :style="{ marginTop: '30px'}"
83
+          >{{btnText}}</el-button>
84
+      </el-col>
85
+    </el-row>
86
+
87
+  </el-dialog>
55 88
 </div>
56 89
 </template>
57 90
 
@@ -63,18 +96,49 @@ const {mapState: mapDeviceState, mapActions: mapDeviceActions} = createNamespace
63 96
 export default {
64 97
   data () {
65 98
     return {
99
+      ws: undefined,
100
+      wsId: undefined,
66 101
       currentPage: 1,
67
-      pageSize: 20
102
+      pageSize: 20,
103
+      checkDevice: undefined,
104
+      dipatchProcess: {
105
+        step: 'ready',
106
+        showDialog: false,
107
+        total: 0,
108
+        current: 0,
109
+        curPer: 0,
110
+        wsId: undefined,
111
+        exception: false,
112
+      },
113
+      lastTime: undefined,
114
+      duration: undefined,
68 115
     }
69 116
   },
70 117
   computed: {
71 118
     ...mapDeviceState({
72 119
       devices: x => x.devices
73
-    })
120
+    }),
121
+    dealInfo () {
122
+      const { current, total, exception } =  this.dipatchProcess
123
+      const percentage = !total || total == 0 ? 0 : window.Math.ceil(current * 100 / total)
124
+      
125
+      const status = exception ? 'exception' : ( percentage >= 100 ? 'success' : 'text' )
126
+
127
+      return { percentage, status }
128
+    },
129
+    btnText () {
130
+      if (this.dipatchProcess.step == 'finish') return '下发完成'
131
+      if (this.dipatchProcess.step == 'process') return '进行中'
132
+
133
+      if (this.dipatchProcess.exception) return '重试'
134
+
135
+      return '开始'
136
+    },
74 137
   },
75 138
   methods: {
76 139
     ...mapDeviceActions([
77
-      'getDeviceList'
140
+      'getDeviceList',
141
+      'dispatchBatch',
78 142
     ]),
79 143
     GetIndex (inx) {
80 144
       return (this.currentPage - 1) * this.pageSize + inx + 1
@@ -89,6 +153,125 @@ export default {
89 153
         return ''
90 154
       }
91 155
     },
156
+    gotoSnapshotList (row) {
157
+      this.$router.push({ name: 'snapshotlist', params: { device: row.deviceId } })
158
+    },
159
+    startDispatch (dev) {
160
+      this.checkDevice = dev
161
+      this.dipatchProcess = {
162
+        step: 'ready',
163
+        showDialog: true,
164
+        total: 0,
165
+        current: 0,
166
+        curPer: 0,
167
+        wsId: undefined,
168
+        exception: false,
169
+      }
170
+      this.lastTime = undefined
171
+      this.duration = undefined
172
+    },
173
+    dispatchToDevice () {
174
+      this.newWs().then(() => {
175
+
176
+        this.dipatchProcess.step = 'process'
177
+        this.dispatchBatch({
178
+          wsId: this.wsId,
179
+          from: this.dipatchProcess.curPer,
180
+          deviceId: this.checkDevice.deviceId,
181
+          lastNum: this.dipatchProcess.current,
182
+          }).then(() => {
183
+
184
+          // 成功完成下发
185
+          this.dipatchProcess.step = 'finish'
186
+          this.dipatchProcess.current = this.dipatchProcess.total
187
+          this.dipatchProcess.exception = false
188
+          this.lastTime = undefined
189
+          this.duration = undefined
190
+
191
+          this.destroyWs(true)
192
+        }).catch((e) => {
193
+          this.dipatchProcess.exception = true
194
+          this.dipatchProcess.step = 'ready'
195
+          this.lastTime = undefined
196
+          this.duration = undefined
197
+
198
+          this.$notify.error(e.message)
199
+        })
200
+      })
201
+
202
+    },
203
+    destroyWs () {
204
+      if (this.ws) {
205
+        this.ws = undefined
206
+        this.wsId = undefined
207
+      }
208
+    },
209
+    newWs () {
210
+      if (this.ws) return Promise.resolve()
211
+
212
+      return new Promise((resolve) => {
213
+        this.wsId = `admin-${window.Math.random().toString(36).substr(2)}`
214
+
215
+        const wsURL = `${window.location.origin.replace('http', 'ws')}/api/websocket/${this.wsId}`
216
+        this.ws = new WebSocket(wsURL)
217
+
218
+        this.ws.onopen = () => {
219
+          resolve()
220
+        }
221
+        
222
+        // 处理过程中
223
+        this.ws.onmessage = (e) => {
224
+          const { step, current, id, total } =  window.JSON.parse(e.data)
225
+
226
+          switch (step) {
227
+            case 'ready':
228
+              this.dipatchProcess.step = 'process'
229
+              this.dipatchProcess.total = total
230
+              this.dipatchProcess.exception = false
231
+              break
232
+            case 'process':
233
+              this.dipatchProcess.current = current
234
+              this.dipatchProcess.exception = false
235
+              this.dipatchProcess.curPer = id
236
+              this.compDuration()
237
+              break;
238
+            case 'finish':
239
+              break
240
+            default:
241
+              break
242
+          }
243
+        }
244
+
245
+        this.ws.onclose = () => {
246
+          window.console.error('websocket 连接被断开')
247
+
248
+          if (this.dipatchProcess.step == 'process') {
249
+            this.$notify('进度监控异常, 请耐心等待完成')
250
+          }
251
+
252
+          this.destroyWs()
253
+        },
254
+
255
+        this.ws.onerror = (e) => {
256
+          window.console.error(e)
257
+        }
258
+      })
259
+    },
260
+    compDuration () {
261
+      const { current, total } =  this.dipatchProcess
262
+      if (!total || total == 0) return 0
263
+
264
+      const now = new Date()
265
+      if (!this.lastTime) {
266
+        this.lastTime = now
267
+        return
268
+      }
269
+
270
+      const durPer = now.valueOf() - this.lastTime.valueOf()
271
+      const totalMin = (total - current) * durPer / ( 1000 * 60 )
272
+      this.duration = window.Number(totalMin).toFixed(1)
273
+      this.lastTime = now
274
+    }
92 275
   },
93 276
   created () {
94 277
     this.getDevices()

+ 158
- 0
src/views/device/snapshot.vue Visa fil

@@ -0,0 +1,158 @@
1
+<template>
2
+<div>
3
+  <xm-search @submit="search">
4
+    <el-form-item>
5
+      <el-date-picker
6
+        v-model="filterData.dateRange"
7
+        type="daterange"
8
+        range-separator="至"
9
+        start-placeholder="开始日期"
10
+        end-placeholder="结束日期">
11
+      </el-date-picker>
12
+    </el-form-item>
13
+  </xm-search>
14
+
15
+  <el-card shadow="hover">
16
+    <el-table
17
+      :data="list"
18
+      style="width: 100%">
19
+      <el-table-column
20
+        label="抓拍日期"
21
+      >
22
+        <template slot-scope="scope">
23
+          <span>{{ formatDate(scope.row.createDate) }}</span>
24
+        </template>
25
+      </el-table-column>
26
+      <el-table-column
27
+        prop="personId"
28
+        label="入库ID">
29
+      </el-table-column>
30
+      <el-table-column
31
+        label="抓拍图">
32
+        <template slot-scope="scope">
33
+          <img class="avatar" :src="scope.row.avatar" alt="">
34
+        </template>
35
+      </el-table-column>
36
+      <el-table-column
37
+        prop="similarity"
38
+        label="相似度">
39
+      </el-table-column>
40
+      <el-table-column
41
+        label="匹配人">
42
+        <template slot-scope="scope">
43
+          <span>{{ scope.row.matchPerson > 0 ? `${scope.row.matchName || ''}(${scope.row.matchPerson})` : '' }}</span>
44
+        </template>
45
+      </el-table-column>
46
+      <el-table-column
47
+        label="匹配图">
48
+        <template slot-scope="scope">
49
+          <img class="avatar" :src="scope.row.matchAvatar" alt="">
50
+        </template>
51
+      </el-table-column>
52
+      <el-table-column
53
+        prop="score"
54
+        label="人像质量(0-100)">
55
+      </el-table-column>
56
+      <el-table-column
57
+        label="比对结果">
58
+        <template slot-scope="scope">
59
+          <el-tag v-if="scope.row.status == 1" type="success">成功</el-tag>
60
+          <el-tag v-else-if="scope.row.status == 2" type="danger">失败</el-tag>
61
+          <el-tag v-else type="info">未知</el-tag>
62
+        </template>
63
+      </el-table-column>
64
+    </el-table>
65
+  </el-card>
66
+  <el-pagination
67
+    small
68
+    style="margin-top:10px;"
69
+    layout="prev, pager, next"
70
+    :current-page="pageNavi.current"
71
+    :pageSize="pageNavi.size"
72
+    :total="pageNavi.total"
73
+    @current-change="handlePageChange"
74
+  >
75
+  </el-pagination>
76
+</div>
77
+</template>
78
+
79
+<script>
80
+import { createNamespacedHelpers } from 'vuex'
81
+import dayjs from 'dayjs';
82
+
83
+const {mapState, mapActions } = createNamespacedHelpers('device')
84
+
85
+export default {
86
+  data () {
87
+    return {
88
+      deviceId: undefined,
89
+      filterData: {
90
+        dateRange: [],
91
+      },
92
+    }
93
+  },
94
+  computed: {
95
+    ...mapState({
96
+      dtSource: x => x.snapshots,
97
+    }),
98
+    startDate () {
99
+      if (!this.filterData.dateRange || this.filterData.dateRange.length < 1) {
100
+        return undefined;
101
+      }
102
+
103
+      return dayjs(this.filterData.dateRange[0]).format('YYYY-MM-DDT00:00:00')
104
+    },
105
+    endDate () {
106
+      if (!this.filterData.dateRange || this.filterData.dateRange.length < 2) {
107
+        return undefined;
108
+      }
109
+
110
+      return dayjs(this.filterData.dateRange[1]).format('YYYY-MM-DDT23:59:59')
111
+    },
112
+    list () {
113
+      const { records = [] } = this.dtSource || {}
114
+      return records
115
+    },
116
+    pageNavi () {
117
+      const { current = 1, size = 20, total = 0 } = this.dtSource || {}
118
+      return { current, size, total }
119
+    }
120
+  },
121
+  methods: {
122
+    ...mapActions([
123
+      'getSnapshots'
124
+    ]),
125
+    handlePageChange (nwPage) {
126
+      this.search(nwPage)
127
+    },
128
+    search (page) {
129
+      this.getSnapshots({
130
+        pageNum: page || this.pageNavi.current,
131
+        pageSize: this.pageNavi.size,
132
+        deviceId: this.deviceId,
133
+        startDate: this.startDate,
134
+        endDate: this.endDate,
135
+      })
136
+    },
137
+    formatDate (dt) {
138
+      if (!dt || dt.indexOf('0001-') > -1) return ''
139
+      return dayjs(dt).format('YYYY-MM-DD HH:mm:ss')
140
+    },
141
+    init () {
142
+      this.deviceId = this.$route.params.device
143
+      this.filterData.dateRange = [dayjs().startOf('week').toDate(), new Date()]
144
+    }
145
+  },
146
+  created () {
147
+    this.init()
148
+    this.search()
149
+  }
150
+}
151
+</script>
152
+
153
+<style lang="scss" scoped>
154
+.avatar {
155
+  width: 64px;
156
+  height: 64px;
157
+}
158
+</style>

+ 11
- 3
src/views/index.js Visa fil

@@ -1,10 +1,9 @@
1 1
 
2 2
 const pages = [  
3 3
   {
4
-    path: '',
4
+    path: 'dashboard',
5 5
     name: 'dashboard',
6 6
     component: () => import('./Dashboard.vue'),
7
-    redirect: { name: 'personlist' },
8 7
     meta: {
9 8
       menuShow: true,
10 9
       title: 'Dashboard',
@@ -20,7 +19,7 @@ const pages = [
20 19
     },
21 20
     children: [
22 21
       {
23
-        path: 'devicelist',
22
+        path: 'device',
24 23
         name: 'devicelist',
25 24
         component: () => import('./device/list.vue'),
26 25
         meta: {
@@ -28,6 +27,15 @@ const pages = [
28 27
           title: '设备管理',
29 28
         },
30 29
       },
30
+      {
31
+        path: 'snapshot/:device',
32
+        name: 'snapshotlist',
33
+        component: () => import('./device/snapshot.vue'),
34
+        meta: {
35
+          menuShow: false,
36
+          title: '抓拍列表',
37
+        },
38
+      },
31 39
       {
32 40
         path: 'paramlist',
33 41
         name: 'paramlist',

+ 22
- 4
src/views/person/edit.vue Visa fil

@@ -5,6 +5,9 @@
5 5
         <el-form-item label="会员ID">
6 6
           <el-input v-model="memProfile.personId" readonly></el-input>
7 7
         </el-form-item>
8
+        <el-form-item label="真实ID">
9
+          <el-input v-model="memProfile.realId" readonly></el-input>
10
+        </el-form-item>
8 11
         <el-form-item label="会员姓名">
9 12
           <el-input v-model="memProfile.name"></el-input>
10 13
         </el-form-item>
@@ -47,14 +50,16 @@
47 50
           <el-row>
48 51
             <el-button type="primary" @click="onSubmit">保存</el-button>
49 52
             <el-tooltip content="同步数据至各抓拍机" placement="top">
50
-              <el-button type="primary" class="right-space" @click="downFace" :loading="optLoading.sync" :disabled="!(memProfile.personId && memProfile.avatar)">同步</el-button>
53
+              <el-button type="warn" class="right-space" @click="downFace" :loading="optLoading.sync" :disabled="!(memProfile.personId && memProfile.avatar)">同步</el-button>
51 54
             </el-tooltip>
55
+            <!--
52 56
             <el-tooltip content="下发数据至各门禁" placement="top">
53 57
               <el-button type="success" @click="showDispatchDialog = true" :disabled="!(memProfile.personId && memProfile.avatar)">下发</el-button>
54 58
             </el-tooltip>
55 59
             <el-tooltip content="禁用各门禁权限" placement="top">
56 60
               <el-button type="danger" class="right-space" :loading="optLoading.forbid" :disabled="!(memProfile.personId && memProfile.avatar)" @click="deleteFromGuard">禁用</el-button>
57 61
             </el-tooltip>
62
+            -->
58 63
             <el-button @click="onCancel">取消</el-button>
59 64
           </el-row>
60 65
         </el-form-item>
@@ -71,7 +76,7 @@
71 76
       </el-dialog>
72 77
     </el-tab-pane>
73 78
     <el-tab-pane label="来访记录" name="visiting">
74
-      <visitinglog :person="memProfile.personId" />
79
+      <visitinglog :person="memProfile.personId" :sales-list="salesList" @change="handleLogUpdate" />
75 80
     </el-tab-pane>
76 81
   </el-tabs>
77 82
 </template>
@@ -81,6 +86,7 @@ import { createNamespacedHelpers } from 'vuex'
81 86
 import apis from '../../config/api'
82 87
 
83 88
 const { mapState: mapTypeState, mapActions: mapTypeActions } = createNamespacedHelpers('type')
89
+const { mapState: mapUserState, mapActions: mapUserActions } = createNamespacedHelpers('sysuser')
84 90
 const { mapState: mapMemberState, mapActions: mapMemberActions, mapMutations: mapMemberMutations } = createNamespacedHelpers('person')
85 91
 
86 92
 export default {
@@ -110,9 +116,12 @@ export default {
110 116
     }),
111 117
     ...mapMemberState({
112 118
       detail: x => x.detail,
113
-      logs: x => x.visitingLog,
119
+      // logs: x => x.visitingLog,
114 120
       guards: x => x.guards,
115 121
     }),
122
+    ...mapUserState({
123
+      salesList: x => (x.list || {}).records || [],
124
+    }),
116 125
     memProfile: {
117 126
       get () {
118 127
         return this.detail
@@ -127,7 +136,8 @@ export default {
127 136
       'getList'
128 137
     ]),
129 138
     ...mapMemberMutations([
130
-      'updateDetail'
139
+      'updateDetail',
140
+      'updateLog',
131 141
     ]),
132 142
     ...mapMemberActions([
133 143
       'getDetail',
@@ -138,6 +148,9 @@ export default {
138 148
       'forbidGuard',
139 149
       'getGuards',
140 150
     ]),
151
+    ...mapUserActions([
152
+      'getSysUserList',
153
+    ]),
141 154
     onCancel () {
142 155
       // this.$router.push('personlist')
143 156
       this.$router.go(-1)
@@ -165,6 +178,9 @@ export default {
165 178
       this.imgurl = res.message
166 179
       this.loading.close()
167 180
     },
181
+    handleLogUpdate (log) {
182
+      this.updateLog(log)
183
+    },
168 184
     onSubmit () {
169 185
       if (!this.memProfile.personId) {
170 186
         // 新增
@@ -234,6 +250,8 @@ export default {
234 250
 
235 251
       this.getGuards()
236 252
 
253
+      this.getSysUserList({ pageNum: 1, pageSize: 999, isSales: 1 })
254
+
237 255
       this.getList({
238 256
         pageNum: 1,
239 257
         pageSize: 999

+ 73
- 23
src/views/person/list.vue Visa fil

@@ -32,15 +32,6 @@
32 32
     <el-card shadow="hover">
33 33
       <div slot="header" class="clearfix">
34 34
         <span>
35
-          <el-select v-model="activeType" @change="handleTypeSelect" placeholder="请选择">
36
-            <el-option label="全部" value="%"></el-option>
37
-            <el-option
38
-              v-for="item in (types.records || [])"
39
-              :key="item.typeId"
40
-              :label="item.typeName"
41
-              :value="`${item.typeId}`">
42
-            </el-option>
43
-          </el-select>
44 35
         </span>
45 36
         <el-button
46 37
           type="primary"
@@ -58,30 +49,48 @@
58 49
           prop="personId">
59 50
         </el-table-column>
60 51
         <el-table-column
61
-          label="照片"
62
-          width="180">
52
+          label="照片">
63 53
           <template slot-scope="scope">
64 54
             <div class="avatar">
65 55
               <img :src="scope.row.avatar" alt="" />
66 56
             </div>
67 57
           </template>
68 58
         </el-table-column>
69
-        <el-table-column
70
-          label="分类"
71
-        >
59
+        <el-table-column width="180">
60
+          <template slot="header" slot-scope="scope">
61
+            <el-dropdown @command="handleTypeSelect" trigger="click">
62
+              <span>
63
+                {{handleTitleName(scope) || '所属分类'}}
64
+                <i class="el-icon-arrow-down el-icon--right"></i>
65
+              </span>
66
+              <el-dropdown-menu slot="dropdown">
67
+                <el-dropdown-item command="%">全部</el-dropdown-item>
68
+                <el-dropdown-item
69
+                  v-for="item in (types.records || [])"
70
+                  :key="item.typeId"
71
+                  :command="`${item.typeId}`">
72
+                  {{item.typeName}}
73
+                </el-dropdown-item>
74
+              </el-dropdown-menu>
75
+            </el-dropdown>
76
+          </template>
72 77
           <template slot-scope="scope">
73
-            <span>{{getTypeName(scope.row.typeId)}}</span>
78
+            <editable-select :dict="typeDict" :value="scope.row.typeId" v-on="{ change: handleRowChange(scope.row, 'typeId') }"></editable-select>
74 79
           </template>
75 80
         </el-table-column>
76 81
         <el-table-column
77
-          prop="name"
78
-          label="姓名"
82
+          label="姓名">
83
+          <template slot-scope="scope">
84
+            <editable-input :value="scope.row.name" v-on="{ change: handleRowChange(scope.row, 'name') }"></editable-input>
85
+          </template>
79 86
         >
80 87
         </el-table-column>
81 88
         <el-table-column
82
-          prop="phone"
83 89
           label="电话"
84 90
         >
91
+          <template slot-scope="scope">
92
+            <editable-input :value="scope.row.phone" v-on="{ change: handleRowChange(scope.row, 'phone') }"></editable-input>
93
+          </template>
85 94
         </el-table-column>
86 95
         <el-table-column
87 96
           label="设备名称"
@@ -100,10 +109,10 @@
100 109
         </el-table-column>
101 110
         <el-table-column
102 111
           fixed="right"
103
-          label="操作"
104
-          width="100">
112
+          label="操作">
105 113
           <template slot-scope="scope">
106 114
             <el-button type="text" @click="handleClick(scope.row, 'detail')" size="small">编辑</el-button>
115
+            <el-button type="text" @click="showMergeDialog(scope.row)" size="small">合并</el-button>
107 116
             <el-button type="text" @click="handleClick(scope.row, 'visiting')" size="small">来访</el-button>
108 117
             <!-- <el-button @click="handleDel(scope.row)" type="text" size="small">删除</el-button> -->
109 118
           </template>
@@ -124,6 +133,13 @@
124 133
       >
125 134
       </el-pagination>
126 135
     </xm-rtl>
136
+    <el-dialog title="人员合并" :visible.sync="mergeDialogVisible">
137
+      <merge-person
138
+        :from="mergeFrom"
139
+        :person-types="(types || {}).records"
140
+        @cancel="mergeDialogVisible = false"
141
+      ></merge-person>
142
+    </el-dialog>
127 143
   </div>
128 144
 </template>
129 145
 
@@ -132,13 +148,16 @@ import dayjs from 'dayjs'
132 148
 import { createNamespacedHelpers } from 'vuex'
133 149
 
134 150
 const {mapState: mapTypeState, mapActions: mapTypeActions} = createNamespacedHelpers('type')
135
-const {mapState: mapMemberState, mapActions: mapMemberActions} = createNamespacedHelpers('person')
151
+const {mapState: mapMemberState, mapActions: mapMemberActions, mapMutations: mapMemberMutations } = createNamespacedHelpers('person')
136 152
 const {mapState: mapDeviceState, mapActions: mapDeviceActions} = createNamespacedHelpers('device')
137 153
 
138 154
 export default {
139 155
   name: 'person-list',
140 156
   components: {
141
-    // visitinglog: () => import('./visitinglog.vue')
157
+    // visitinglog: () => import('./visitinglog.vue'),
158
+    editableInput: () => import('@/components/EditableInput.vue'),
159
+    editableSelect: () => import('@/components/EditableSelect.vue'),
160
+    mergePerson: () => import('./merge.vue')
142 161
   },
143 162
   data () {
144 163
     return {
@@ -151,7 +170,9 @@ export default {
151 170
       },
152 171
       activeType: '%',
153 172
       commPrefix: '',
173
+      mergeFrom: {},
154 174
       dialogVisible: false,
175
+      mergeDialogVisible: false,
155 176
       searchData: {},
156 177
       initPage: 1,
157 178
       currentPerson: 0,
@@ -175,19 +196,44 @@ export default {
175 196
     perType () {
176 197
       return !this.activeType || this.activeType === '%' ? 0 : this.activeType
177 198
     },
199
+    typeDict () {
200
+      return ((this.types || {}).records || []).map((item) => {
201
+        return {
202
+          value: item.typeId,
203
+          label: item.typeName,
204
+        }
205
+      })
206
+    }
178 207
   },
179 208
   methods: {
180 209
     ...mapTypeActions([
181 210
       'getList'
182 211
     ]),
212
+    ...mapMemberMutations([
213
+      'mergeList',
214
+    ]),
183 215
     ...mapMemberActions([
184 216
       'getPersonList',
217
+      'editPerson',
185 218
       'setDetailNull',
186 219
       'deletePerson',
187 220
     ]),
188 221
     ...mapDeviceActions([
189 222
       'getDeviceList'
190 223
     ]),
224
+    handleTitleName () { return undefined },
225
+    handleRowChange (row, key) {
226
+      return (val) => {
227
+        const person = { ...row, [`${key}`]: val }
228
+
229
+        this.editPerson({
230
+          onSuccess: () => {
231
+            this.mergeList(person)
232
+          },
233
+          detail: JSON.stringify(person)
234
+        })
235
+      }
236
+    },
191 237
     handleTypeSelect (tab) {
192 238
       const perType = !tab || tab === '%' ? 0 : tab
193 239
       this.refreshPage({ tab: perType })
@@ -247,6 +293,10 @@ export default {
247 293
         })
248 294
       })
249 295
     },
296
+    showMergeDialog(row) {
297
+      this.mergeFrom = row
298
+      this.mergeDialogVisible = true
299
+    },
250 300
     getSelectVal () {
251 301
       const bdt = this.filterData.beginDate ? dayjs(this.filterData.beginDate).format('YYYY-MM-DDTHH:mm:ss') : '';
252 302
       const edt = this.filterData.endDate ? dayjs(this.filterData.endDate).format('YYYY-MM-DDTHH:mm:ss') : ''
@@ -286,7 +336,7 @@ export default {
286 336
     //   this.currentPerson = row.personId
287 337
     // },
288 338
     initData () {
289
-      this.activeType = this.$route.query.tab || '%'
339
+      this.activeType = this.$route.query.tab == 0 || this.$route.query.tab == undefined ? '%' : this.$route.query.tab
290 340
       this.initPage = (this.$route.query.page || 1) - 0
291 341
 
292 342
       this.getDeviceList({

+ 196
- 0
src/views/person/merge.vue Visa fil

@@ -0,0 +1,196 @@
1
+<template>
2
+  <div class="merge-box">
3
+    <el-row class="merge-item merge-head" type="flex">
4
+      <el-col :span="2">#</el-col>
5
+      <el-col :span="5">待合并</el-col>
6
+      <el-col :span="1">&nbsp;</el-col>
7
+      <el-col :span="5">合并目标</el-col>
8
+      <el-col :span="1">&nbsp;</el-col>
9
+      <el-col :span="10">合并结果</el-col>
10
+    </el-row>
11
+    <el-row class="merge-item">
12
+      <el-col :span="2">人员ID</el-col>
13
+      <el-col :span="5">{{ from.personId || '&nbsp;' }}</el-col>
14
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
15
+      <el-col :span="5">
16
+        <el-input size="small" v-model="to.personId" @keyup.enter.native="getMergeTo"></el-input>
17
+      </el-col>
18
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
19
+      <el-col :span="10">
20
+        <el-input size="small" v-model="result.personId" readonly></el-input>
21
+      </el-col>
22
+    </el-row>
23
+    <el-row class="merge-item">
24
+      <el-col :span="2">识别ID</el-col>
25
+      <el-col :span="5">{{ from.realId || '&nbsp;' }}</el-col>
26
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
27
+      <el-col :span="5">{{ to.realId || '&nbsp;' }}</el-col>
28
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
29
+      <el-col :span="10">
30
+        <el-input size="small" v-model="result.realId" readonly></el-input>
31
+      </el-col>
32
+    </el-row>
33
+    <el-row class="merge-item">
34
+      <el-col :span="2">姓名</el-col>
35
+      <el-col :span="5">{{ from.name || '&nbsp;' }}</el-col>
36
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
37
+      <el-col :span="5">{{ to.name || '&nbsp;' }}</el-col>
38
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
39
+      <el-col :span="10">
40
+        <el-input size="small" v-model="result.name"></el-input>
41
+      </el-col>
42
+    </el-row>
43
+    <el-row class="merge-item">
44
+      <el-col :span="2">分类</el-col>
45
+      <el-col :span="5">{{ getTypeName(from.typeId) || '&nbsp;' }}</el-col>
46
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
47
+      <el-col :span="5">{{ getTypeName(to.typeId) || '&nbsp;' }}</el-col>
48
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
49
+      <el-col :span="10">
50
+        <el-select :style="{ width: '100%' }" size="small" v-model="result.typeId">
51
+          <el-option v-for="type in (personTypes || [])" :key="type.typeId" :label="type.typeName" :value="type.typeId"></el-option>
52
+        </el-select>
53
+      </el-col>
54
+    </el-row>
55
+    <el-row class="merge-item">
56
+      <el-col :span="2">手机</el-col>
57
+      <el-col :span="5">{{ from.phone || '&nbsp;' }}</el-col>
58
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
59
+      <el-col :span="5">{{ to.phone || '&nbsp;' }}</el-col>
60
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
61
+      <el-col :span="10">
62
+        <el-input size="small" v-model="result.phone"></el-input>
63
+      </el-col>
64
+    </el-row>
65
+    <el-row class="merge-item">
66
+      <el-col :span="2">邮箱</el-col>
67
+      <el-col :span="5">{{ from.email || '&nbsp;' }}</el-col>
68
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
69
+      <el-col :span="5">{{ to.email || '&nbsp;' }}</el-col>
70
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
71
+      <el-col :span="10">
72
+        <el-input size="small" v-model="result.email"></el-input>
73
+      </el-col>
74
+    </el-row>
75
+    <el-row class="merge-item">
76
+      <el-col :span="2">性别</el-col>
77
+      <el-col :span="5">{{ from.sex == 1 ? '男' : '女' }}</el-col>
78
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
79
+      <el-col :span="5">{{ to.sex == 1 ? '男' : '女' }}</el-col>
80
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
81
+      <el-col :span="10">
82
+        <el-select :style="{ width: '100%' }" size="small" v-model="result.sex">
83
+          <el-option label="男" value="1"></el-option>
84
+          <el-option label="女" value="2"></el-option>
85
+        </el-select>
86
+      </el-col>
87
+    </el-row>
88
+    <el-row class="merge-item">
89
+      <el-col :span="2">头像</el-col>
90
+      <el-col :span="5"><img class="avatar" :src="from.avatar" alt=""></el-col>
91
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
92
+      <el-col :span="5"><img class="avatar" :src="to.avatar" alt=""></el-col>
93
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
94
+      <el-col :span="10">头像不处理, 两个头像均会被保留</el-col>
95
+    </el-row>
96
+    <el-row class="merge-item">
97
+      <el-col :span="2">欢迎语</el-col>
98
+      <el-col :span="5">{{ from.words || '&nbsp;' }}</el-col>
99
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
100
+      <el-col :span="5">{{ to.words || '&nbsp;' }}</el-col>
101
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
102
+      <el-col :span="10">
103
+        <el-input size="small" v-model="result.words"></el-input>
104
+      </el-col>
105
+    </el-row>
106
+    <el-row class="merge-item">
107
+      <el-col :span="2">备注</el-col>
108
+      <el-col :span="5">{{ from.remark || '&nbsp;' }}</el-col>
109
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
110
+      <el-col :span="5">{{ to.remark || '&nbsp;' }}</el-col>
111
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
112
+      <el-col :span="10">
113
+        <el-input size="small" v-model="result.remark"></el-input>
114
+      </el-col>
115
+    </el-row>
116
+    <div :style="{ margin: '20px auto', width: '300px' }">
117
+      <el-button @click="handleCancel" :style="{ marginRight: '48px' }">取消</el-button>
118
+      <el-button type="primary" @click="submit" >确定</el-button>
119
+    </div>
120
+  </div>
121
+</template>
122
+
123
+<script>
124
+import { createNamespacedHelpers } from 'vuex'
125
+
126
+const { mapActions } = createNamespacedHelpers('person')
127
+
128
+export default {
129
+  name: 'person-merge',
130
+  props: [
131
+    'from',
132
+    'personTypes',
133
+  ],
134
+  data () {
135
+    return {
136
+      to: {
137
+        personId: undefined,
138
+      },
139
+      result: {},
140
+    }
141
+  },
142
+  computed: {
143
+  },
144
+  methods: {
145
+    ...mapActions([
146
+      'getPurPerson',
147
+      'mergePerson',
148
+    ]),
149
+    getMergeTo () {
150
+      this.getPurPerson({ id: this.to.personId }).then(({ detail }) => {
151
+        this.to = detail
152
+        this.result = detail
153
+      })
154
+    },
155
+    getTypeName (typeId) {
156
+      return ((this.personTypes || []).filter(x => x.typeId === typeId)[0] || {}).typeName
157
+    },
158
+    handleCancel () {
159
+      this.$emit('cancel')
160
+    },
161
+    submit() {
162
+      this.mergePerson({ from: this.from.personId, to: this.result }).then(() => {
163
+        this.$notify.success('合并成功')
164
+      })
165
+    }
166
+  }
167
+}
168
+</script>
169
+
170
+<style lang="scss" scoped>
171
+.merge-box {
172
+
173
+  .merge-head {
174
+    font-size: 1.2em;
175
+    font-weight: 500;
176
+  }
177
+
178
+  .merge-item {
179
+    padding: 12px 0;
180
+
181
+    .el-col {
182
+      text-align: center;
183
+      line-height: 32px;
184
+    }
185
+
186
+    & + .merge-item {
187
+      border-top: 1px solid #ebeef5;
188
+    }
189
+
190
+    .avatar {
191
+      width: 64px;
192
+      height: 64px;
193
+    }
194
+  }
195
+}
196
+</style>

+ 36
- 3
src/views/person/visitinglog.vue Visa fil

@@ -57,6 +57,16 @@
57 57
           label="来访目的"
58 58
         >
59 59
         </el-table-column>
60
+        <el-table-column
61
+          prop="salesName"
62
+          label="销售"
63
+        >
64
+        </el-table-column>
65
+        <el-table-column
66
+          prop="remark"
67
+          label="备注"
68
+        >
69
+        </el-table-column>
60 70
         <el-table-column
61 71
           fixed="right"
62 72
           label="操作"
@@ -87,6 +97,16 @@
87 97
           <el-form-item label-width="120px" label="来访目的">
88 98
             <el-input v-model="logDetail.visitPurpose"></el-input>
89 99
           </el-form-item>
100
+          <el-form-item label-width="120px" label="跟踪销售">
101
+            <el-select v-model="logDetail.salesId" @change="handleSalesChange" placeholder="请选择">
102
+              <el-option
103
+                v-for="sales in salesList"
104
+                :key="sales.userId"
105
+                :label="sales.nickname"
106
+                :value="sales.userId">
107
+              </el-option>
108
+            </el-select>
109
+          </el-form-item>
90 110
           <el-form-item label-width="120px" label="备注">
91 111
             <el-input type="textarea" rows="10" v-model="logDetail.remark"></el-input>
92 112
           </el-form-item>
@@ -129,6 +149,7 @@ export default {
129 149
   props: [
130 150
     'person',
131 151
     'pageSize',
152
+    'salesList',
132 153
   ],
133 154
   data () {
134 155
     return {
@@ -140,7 +161,7 @@ export default {
140 161
         total: 0,
141 162
         size: 10,
142 163
       },
143
-      logDetail: {}
164
+      logDetail: {},
144 165
     }
145 166
   },
146 167
   computed: {
@@ -148,6 +169,11 @@ export default {
148 169
       visitingLogs: x => x.visitingLog,
149 170
     }),
150 171
   },
172
+  watch: {
173
+    person () {
174
+      this.getDataPaged()
175
+    }
176
+  },
151 177
   created () {
152 178
     this.getDataPaged()
153 179
   },
@@ -161,6 +187,9 @@ export default {
161 187
     ...mapMemberMutations([
162 188
       'resetVisitingLog',
163 189
     ]),
190
+    handleSalesChange (salesId) {
191
+      this.logDetail.salesName = this.salesList.filter(x => x.userId === salesId)[0].nickname
192
+    },
164 193
     getDataPaged (page = 1) {
165 194
       if (!this.person) return
166 195
 
@@ -179,11 +208,15 @@ export default {
179 208
       this.editVisibleLog({detail:JSON.stringify(this.logDetail), logId: this.logDetail.logId}).then(() => {
180 209
         this.dialogShow = false
181 210
 
211
+        this.visitingLogs = this.visitingLogs.map((log) => {
212
+          return log.logId === this.logDetail.logId ? { ...this.logDetail } : log
213
+        })
214
+
182 215
         this.$emit('change', this.logDetail)
183 216
       })
184 217
     },
185 218
     handleEdit (dt) {
186
-      this.logDetail = dt
219
+      this.logDetail = { ...dt }
187 220
       this.dialogShow = true
188 221
     },
189 222
     dispatchHik (dt) {
@@ -204,7 +237,7 @@ export default {
204 237
         return ''
205 238
       }
206 239
 
207
-      return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
240
+      return dayjs(date).format('YYYY-MM-DD HH:mm')
208 241
     },
209 242
     pageChange (page) {
210 243
       this.$emit("pageChange", page)

+ 23
- 0
src/views/statis/visiting/index.vue Visa fil

@@ -0,0 +1,23 @@
1
+<template>
2
+<div>
3
+  <xm-search></xm-search>
4
+  <el-card shadow="hover">
5
+    
6
+  </el-card>
7
+  <el-el-pagination></el-el-pagination>
8
+</div>
9
+</template>
10
+
11
+<script>
12
+  export default {
13
+    name: 'sta-visiting',
14
+    data () {
15
+      return {
16
+
17
+      }
18
+    },
19
+    methods: {
20
+
21
+    },
22
+  }
23
+</script>

+ 14
- 12
src/views/words/edit.vue Visa fil

@@ -23,9 +23,11 @@
23 23
     <el-form-item label="欢迎语" prop="words" required>
24 24
       <el-input v-model="detail.words"></el-input>
25 25
     </el-form-item>
26
+    <!--
26 27
     <el-form-item label="权重" prop="weight" required>
27 28
       <el-input type="number" v-model="detail.weight"></el-input>
28 29
     </el-form-item>
30
+    -->
29 31
     <el-form-item label="状态" prop="status" required>
30 32
       <el-select v-model="detail.status" placeholder="请选择">
31 33
         <el-option label="禁用" :value="0"></el-option>
@@ -64,18 +66,18 @@ export default {
64 66
         status: [
65 67
           { required: true, message: '请选择状态', trigger: 'change' }
66 68
         ],
67
-        weight: [{
68
-          type: 'integer',
69
-          required: true,
70
-          trigger: 'blur',
71
-          validator: (rule, value, callback) => {
72
-            if (value <= 1 && value >= 0.1) {
73
-              callback()
74
-            } else {
75
-              callback(new Error('权重值 0.1 ~ 1, 默认 0.85'))
76
-            }
77
-          }
78
-        }],
69
+        // weight: [{
70
+        //   type: 'integer',
71
+        //   required: true,
72
+        //   trigger: 'blur',
73
+        //   validator: (rule, value, callback) => {
74
+        //     if (value <= 1 && value >= 0.1) {
75
+        //       callback()
76
+        //     } else {
77
+        //       callback(new Error('权重值 0.1 ~ 1, 默认 0.85'))
78
+        //     }
79
+        //   }
80
+        // }],
79 81
       }
80 82
     }
81 83
   },

+ 17
- 5
vue.config.js Visa fil

@@ -3,12 +3,24 @@ module.exports = {
3 3
   devServer: {
4 4
     proxy: {
5 5
       '/api': {
6
-        target: 'http://10.168.2.125:8000',
6
+        target: 'http://localhost:8080',
7 7
         changeOrigin: true,
8
-        // pathRewrite: {
9
-        //   '^/api': '/'
10
-        // },
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
+        },
11 19
       },
12 20
     }
13
-  }
21
+  },
22
+  transpileDependencies: [
23
+    'vue-echarts',
24
+    'resize-detector'
25
+  ]
14 26
 }